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, vlq_encode_unsigned};
30use srcmap_scopes::ScopeInfo;
31
32// ── Public types ───────────────────────────────────────────────────
33
34/// A mapping from a generated position to an original position.
35///
36/// Used with [`SourceMapGenerator`] to define position relationships.
37/// All positions are 0-based.
38#[derive(Debug, Clone)]
39pub struct Mapping {
40    /// 0-based line in the generated output.
41    pub generated_line: u32,
42    /// 0-based column in the generated output.
43    pub generated_column: u32,
44    /// Source index from [`SourceMapGenerator::add_source`], or `None` for generated-only.
45    pub source: Option<u32>,
46    /// 0-based line in the original source.
47    pub original_line: u32,
48    /// 0-based column in the original source.
49    pub original_column: u32,
50    /// Name index from [`SourceMapGenerator::add_name`], or `None`.
51    pub name: Option<u32>,
52    /// Whether this mapping is a range mapping (ECMA-426 rangeMappings proposal).
53    pub is_range_mapping: bool,
54}
55
56/// Builder for creating source maps incrementally.
57///
58/// Register sources and names first (they return indices), then add mappings
59/// that reference those indices. Mappings should be added in generated-position
60/// order, though the builder does not require it.
61///
62/// Sources and names are automatically deduplicated.
63///
64/// # Workflow
65///
66/// 1. [`add_source`](Self::add_source) — register each original file
67/// 2. [`set_source_content`](Self::set_source_content) — optionally attach content
68/// 3. [`add_name`](Self::add_name) — register identifier names
69/// 4. [`add_mapping`](Self::add_mapping) / [`add_named_mapping`](Self::add_named_mapping) — add mappings
70/// 5. [`to_json`](Self::to_json) — serialize to JSON
71#[derive(Debug)]
72pub struct SourceMapGenerator {
73    file: Option<String>,
74    source_root: Option<String>,
75    sources: Vec<String>,
76    sources_content: Vec<Option<String>>,
77    names: Vec<String>,
78    mappings: Vec<Mapping>,
79    ignore_list: Vec<u32>,
80    debug_id: Option<String>,
81    scopes: Option<ScopeInfo>,
82
83    // Dedup maps for O(1) lookup
84    source_map: HashMap<String, u32>,
85    name_map: HashMap<String, u32>,
86}
87
88impl SourceMapGenerator {
89    /// Create a new empty source map generator.
90    pub fn new(file: Option<String>) -> Self {
91        Self {
92            file,
93            source_root: None,
94            sources: Vec::new(),
95            sources_content: Vec::new(),
96            names: Vec::new(),
97            mappings: Vec::new(),
98            ignore_list: Vec::new(),
99            debug_id: None,
100            scopes: None,
101            source_map: HashMap::new(),
102            name_map: HashMap::new(),
103        }
104    }
105
106    /// Set the source root prefix.
107    pub fn set_source_root(&mut self, root: impl Into<String>) {
108        self.source_root = Some(root.into());
109    }
110
111    /// Set the debug ID (UUID) for this source map (ECMA-426).
112    pub fn set_debug_id(&mut self, id: impl Into<String>) {
113        self.debug_id = Some(id.into());
114    }
115
116    /// Set scope and variable information (ECMA-426 scopes proposal).
117    pub fn set_scopes(&mut self, scopes: ScopeInfo) {
118        self.scopes = Some(scopes);
119    }
120
121    /// Register a source file and return its index.
122    #[inline]
123    pub fn add_source(&mut self, source: &str) -> u32 {
124        if let Some(&idx) = self.source_map.get(source) {
125            return idx;
126        }
127        let idx = self.sources.len() as u32;
128        self.sources.push(source.to_string());
129        self.sources_content.push(None);
130        self.source_map.insert(source.to_string(), idx);
131        idx
132    }
133
134    /// Set the content for a source file.
135    pub fn set_source_content(&mut self, source_idx: u32, content: impl Into<String>) {
136        if (source_idx as usize) < self.sources_content.len() {
137            self.sources_content[source_idx as usize] = Some(content.into());
138        }
139    }
140
141    /// Register a name and return its index.
142    #[inline]
143    pub fn add_name(&mut self, name: &str) -> u32 {
144        if let Some(&idx) = self.name_map.get(name) {
145            return idx;
146        }
147        let idx = self.names.len() as u32;
148        self.names.push(name.to_string());
149        self.name_map.insert(name.to_string(), idx);
150        idx
151    }
152
153    /// Add a source index to the ignore list.
154    pub fn add_to_ignore_list(&mut self, source_idx: u32) {
155        if !self.ignore_list.contains(&source_idx) {
156            self.ignore_list.push(source_idx);
157        }
158    }
159
160    /// Add a mapping with no source information (generated-only).
161    pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
162        self.mappings.push(Mapping {
163            generated_line,
164            generated_column,
165            source: None,
166            original_line: 0,
167            original_column: 0,
168            name: None,
169            is_range_mapping: false,
170        });
171    }
172
173    /// Add a mapping from generated position to original position.
174    pub fn 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    ) {
182        self.mappings.push(Mapping {
183            generated_line,
184            generated_column,
185            source: Some(source),
186            original_line,
187            original_column,
188            name: None,
189            is_range_mapping: false,
190        });
191    }
192
193    /// Add a mapping with a name.
194    pub fn add_named_mapping(
195        &mut self,
196        generated_line: u32,
197        generated_column: u32,
198        source: u32,
199        original_line: u32,
200        original_column: u32,
201        name: u32,
202    ) {
203        self.mappings.push(Mapping {
204            generated_line,
205            generated_column,
206            source: Some(source),
207            original_line,
208            original_column,
209            name: Some(name),
210            is_range_mapping: false,
211        });
212    }
213
214    /// Add a range mapping from generated position to original position.
215    ///
216    /// A range mapping maps every position from its generated position up to
217    /// (but not including) the next mapping, applying a proportional delta
218    /// to the original position (ECMA-426 `rangeMappings` proposal).
219    pub fn add_range_mapping(
220        &mut self,
221        generated_line: u32,
222        generated_column: u32,
223        source: u32,
224        original_line: u32,
225        original_column: u32,
226    ) {
227        self.mappings.push(Mapping {
228            generated_line,
229            generated_column,
230            source: Some(source),
231            original_line,
232            original_column,
233            name: None,
234            is_range_mapping: true,
235        });
236    }
237
238    /// Add a named range mapping.
239    pub fn add_named_range_mapping(
240        &mut self,
241        generated_line: u32,
242        generated_column: u32,
243        source: u32,
244        original_line: u32,
245        original_column: u32,
246        name: u32,
247    ) {
248        self.mappings.push(Mapping {
249            generated_line,
250            generated_column,
251            source: Some(source),
252            original_line,
253            original_column,
254            name: Some(name),
255            is_range_mapping: true,
256        });
257    }
258
259    /// Add a mapping only if it differs from the previous mapping on the same line.
260    ///
261    /// This skips redundant mappings where the source position is identical
262    /// to the last mapping, which reduces output size without losing information.
263    /// Used by bundlers and minifiers to avoid bloating source maps.
264    pub fn maybe_add_mapping(
265        &mut self,
266        generated_line: u32,
267        generated_column: u32,
268        source: u32,
269        original_line: u32,
270        original_column: u32,
271    ) -> bool {
272        if let Some(last) = self.mappings.last()
273            && last.generated_line == generated_line
274            && last.source == Some(source)
275            && last.original_line == original_line
276            && last.original_column == original_column
277        {
278            return false;
279        }
280        self.add_mapping(
281            generated_line,
282            generated_column,
283            source,
284            original_line,
285            original_column,
286        );
287        true
288    }
289
290    /// Encode all mappings to a VLQ-encoded string.
291    fn encode_mappings(&self) -> String {
292        if self.mappings.is_empty() {
293            return String::new();
294        }
295
296        // Sort mappings by (generated_line, generated_column)
297        let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
298        sorted.sort_unstable_by(|a, b| {
299            a.generated_line
300                .cmp(&b.generated_line)
301                .then(a.generated_column.cmp(&b.generated_column))
302        });
303
304        #[cfg(feature = "parallel")]
305        if sorted.len() >= 4096 {
306            return Self::encode_parallel_impl(&sorted);
307        }
308
309        Self::encode_sequential_impl(&sorted)
310    }
311
312    #[inline]
313    fn encode_sequential_impl(sorted: &[&Mapping]) -> String {
314        let mut out: Vec<u8> = Vec::with_capacity(sorted.len() * 6);
315
316        let mut prev_gen_col: i64 = 0;
317        let mut prev_source: i64 = 0;
318        let mut prev_orig_line: i64 = 0;
319        let mut prev_orig_col: i64 = 0;
320        let mut prev_name: i64 = 0;
321        let mut prev_gen_line: u32 = 0;
322        let mut first_in_line = true;
323
324        for m in sorted {
325            while prev_gen_line < m.generated_line {
326                out.push(b';');
327                prev_gen_line += 1;
328                prev_gen_col = 0;
329                first_in_line = true;
330            }
331
332            if !first_in_line {
333                out.push(b',');
334            }
335            first_in_line = false;
336
337            vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
338            prev_gen_col = m.generated_column as i64;
339
340            if let Some(source) = m.source {
341                vlq_encode(&mut out, source as i64 - prev_source);
342                prev_source = source as i64;
343
344                vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
345                prev_orig_line = m.original_line as i64;
346
347                vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
348                prev_orig_col = m.original_column as i64;
349
350                if let Some(name) = m.name {
351                    vlq_encode(&mut out, name as i64 - prev_name);
352                    prev_name = name as i64;
353                }
354            }
355        }
356
357        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
358        // and we only add b';' and b',' — all valid UTF-8.
359        debug_assert!(out.is_ascii());
360        unsafe { String::from_utf8_unchecked(out) }
361    }
362
363    #[cfg(feature = "parallel")]
364    fn encode_parallel_impl(sorted: &[&Mapping]) -> String {
365        use rayon::prelude::*;
366
367        let max_line = sorted
368            .last()
369            .expect("encode_parallel_impl requires non-empty sorted slice")
370            .generated_line as usize;
371
372        // Build line ranges: (start_idx, end_idx) into sorted slice
373        let mut line_ranges: Vec<(usize, usize)> = vec![(0, 0); max_line + 1];
374        let mut i = 0;
375        while i < sorted.len() {
376            let line = sorted[i].generated_line as usize;
377            let start = i;
378            while i < sorted.len() && sorted[i].generated_line as usize == line {
379                i += 1;
380            }
381            line_ranges[line] = (start, i);
382        }
383
384        // Sequential scan: compute cumulative state at each line boundary
385        let mut states: Vec<(i64, i64, i64, i64)> = Vec::with_capacity(max_line + 1);
386        let mut prev_source: i64 = 0;
387        let mut prev_orig_line: i64 = 0;
388        let mut prev_orig_col: i64 = 0;
389        let mut prev_name: i64 = 0;
390
391        for &(start, end) in &line_ranges {
392            states.push((prev_source, prev_orig_line, prev_orig_col, prev_name));
393            for m in &sorted[start..end] {
394                if let Some(source) = m.source {
395                    prev_source = source as i64;
396                    prev_orig_line = m.original_line as i64;
397                    prev_orig_col = m.original_column as i64;
398                    if let Some(name) = m.name {
399                        prev_name = name as i64;
400                    }
401                }
402            }
403        }
404
405        // Parallel: encode each line independently
406        let encoded_lines: Vec<Vec<u8>> = line_ranges
407            .par_iter()
408            .zip(states.par_iter())
409            .map(|(&(start, end), &(s, ol, oc, n))| {
410                if start == end {
411                    return Vec::new();
412                }
413                encode_mapping_slice(&sorted[start..end], s, ol, oc, n)
414            })
415            .collect();
416
417        // Join with semicolons
418        let total_len = encoded_lines.iter().map(|l| l.len()).sum::<usize>() + max_line;
419        let mut out: Vec<u8> = Vec::with_capacity(total_len);
420        for (i, bytes) in encoded_lines.iter().enumerate() {
421            if i > 0 {
422                out.push(b';');
423            }
424            out.extend_from_slice(bytes);
425        }
426
427        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
428        // and we only add b';' — all valid UTF-8.
429        debug_assert!(out.is_ascii());
430        unsafe { String::from_utf8_unchecked(out) }
431    }
432
433    /// Encode range mappings to a VLQ string.
434    /// Returns `None` if no range mappings exist.
435    fn encode_range_mappings(&self) -> Option<String> {
436        if !self.mappings.iter().any(|m| m.is_range_mapping) {
437            return None;
438        }
439
440        let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
441        sorted.sort_unstable_by(|a, b| {
442            a.generated_line
443                .cmp(&b.generated_line)
444                .then(a.generated_column.cmp(&b.generated_column))
445        });
446
447        let max_line = sorted.last().map_or(0, |m| m.generated_line);
448        let mut out: Vec<u8> = Vec::new();
449        let mut sorted_idx = 0;
450
451        for line in 0..=max_line {
452            if line > 0 {
453                out.push(b';');
454            }
455            let mut prev_offset: u64 = 0;
456            let mut first_on_line = true;
457            let mut line_local_idx: u64 = 0;
458
459            while sorted_idx < sorted.len() && sorted[sorted_idx].generated_line == line {
460                if sorted[sorted_idx].is_range_mapping {
461                    if !first_on_line {
462                        out.push(b',');
463                    }
464                    first_on_line = false;
465                    let delta = line_local_idx - prev_offset;
466                    vlq_encode_unsigned(&mut out, delta);
467                    prev_offset = line_local_idx;
468                }
469                line_local_idx += 1;
470                sorted_idx += 1;
471            }
472        }
473
474        while out.last() == Some(&b';') {
475            out.pop();
476        }
477
478        if out.is_empty() {
479            return None;
480        }
481
482        // SAFETY: vlq_encode_unsigned only pushes ASCII base64 chars,
483        // and we only add b';' and b',' — all valid UTF-8.
484        debug_assert!(out.is_ascii());
485        Some(unsafe { String::from_utf8_unchecked(out) })
486    }
487
488    /// Generate the source map as a JSON string.
489    pub fn to_json(&self) -> String {
490        use std::fmt::Write;
491
492        let mappings = self.encode_mappings();
493
494        // Encode scopes (may introduce names not yet in self.names)
495        let (scopes_str, names_for_json) = if let Some(ref scopes_info) = self.scopes {
496            let mut names = self.names.clone();
497            let s = srcmap_scopes::encode_scopes(scopes_info, &mut names);
498            (Some(s), std::borrow::Cow::Owned(names))
499        } else {
500            (None, std::borrow::Cow::Borrowed(&self.names))
501        };
502
503        let mut json = String::with_capacity(256 + mappings.len());
504        json.push_str(r#"{"version":3"#);
505
506        if let Some(ref file) = self.file {
507            json.push_str(r#","file":"#);
508            json_quote_into(&mut json, file);
509        }
510
511        if let Some(ref root) = self.source_root {
512            json.push_str(r#","sourceRoot":"#);
513            json_quote_into(&mut json, root);
514        }
515
516        // sources
517        json.push_str(r#","sources":["#);
518        for (i, s) in self.sources.iter().enumerate() {
519            if i > 0 {
520                json.push(',');
521            }
522            json_quote_into(&mut json, s);
523        }
524        json.push(']');
525
526        // sourcesContent (only if any content is set)
527        if self.sources_content.iter().any(|c| c.is_some()) {
528            json.push_str(r#","sourcesContent":["#);
529
530            #[cfg(feature = "parallel")]
531            {
532                use rayon::prelude::*;
533
534                let total_content: usize = self
535                    .sources_content
536                    .iter()
537                    .map(|c| c.as_ref().map_or(0, |s| s.len()))
538                    .sum();
539
540                if self.sources_content.len() >= 8 && total_content >= 8192 {
541                    let quoted: Vec<String> = self
542                        .sources_content
543                        .par_iter()
544                        .map(|c| match c {
545                            Some(content) => json_quote(content),
546                            None => "null".to_string(),
547                        })
548                        .collect();
549                    for (i, q) in quoted.iter().enumerate() {
550                        if i > 0 {
551                            json.push(',');
552                        }
553                        json.push_str(q);
554                    }
555                } else {
556                    for (i, c) in self.sources_content.iter().enumerate() {
557                        if i > 0 {
558                            json.push(',');
559                        }
560                        match c {
561                            Some(content) => json_quote_into(&mut json, content),
562                            None => json.push_str("null"),
563                        }
564                    }
565                }
566            }
567
568            #[cfg(not(feature = "parallel"))]
569            for (i, c) in self.sources_content.iter().enumerate() {
570                if i > 0 {
571                    json.push(',');
572                }
573                match c {
574                    Some(content) => json_quote_into(&mut json, content),
575                    None => json.push_str("null"),
576                }
577            }
578
579            json.push(']');
580        }
581
582        // names
583        json.push_str(r#","names":["#);
584        for (i, n) in names_for_json.iter().enumerate() {
585            if i > 0 {
586                json.push(',');
587            }
588            json_quote_into(&mut json, n);
589        }
590        json.push(']');
591
592        // mappings — VLQ string is pure base64/,/; so no escaping needed
593        json.push_str(r#","mappings":""#);
594        json.push_str(&mappings);
595        json.push('"');
596
597        // ignoreList
598        if !self.ignore_list.is_empty() {
599            json.push_str(r#","ignoreList":["#);
600            for (i, &idx) in self.ignore_list.iter().enumerate() {
601                if i > 0 {
602                    json.push(',');
603                }
604                let _ = write!(json, "{idx}");
605            }
606            json.push(']');
607        }
608
609        // rangeMappings (only if any range mappings exist)
610        if let Some(ref range_mappings) = self.encode_range_mappings() {
611            json.push_str(r#","rangeMappings":""#);
612            json.push_str(range_mappings);
613            json.push('"');
614        }
615
616        // debugId
617        if let Some(ref id) = self.debug_id {
618            json.push_str(r#","debugId":"#);
619            json_quote_into(&mut json, id);
620        }
621
622        // scopes (ECMA-426 scopes proposal)
623        if let Some(ref s) = scopes_str {
624            json.push_str(r#","scopes":"#);
625            json_quote_into(&mut json, s);
626        }
627
628        json.push('}');
629        json
630    }
631
632    /// Get the number of mappings.
633    pub fn mapping_count(&self) -> usize {
634        self.mappings.len()
635    }
636
637    /// Directly construct a `SourceMap` from the generator's internal state.
638    ///
639    /// This avoids the encode-then-decode round-trip (VLQ encode to JSON string,
640    /// then re-parse) that would otherwise be needed in composition pipelines.
641    pub fn to_decoded_map(&self) -> srcmap_sourcemap::SourceMap {
642        // Sort mappings by (generated_line, generated_column) — same as encode_mappings
643        let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
644        sorted.sort_unstable_by(|a, b| {
645            a.generated_line
646                .cmp(&b.generated_line)
647                .then(a.generated_column.cmp(&b.generated_column))
648        });
649
650        // Convert generator Mapping → sourcemap Mapping
651        let sm_mappings: Vec<srcmap_sourcemap::Mapping> = sorted
652            .iter()
653            .map(|m| srcmap_sourcemap::Mapping {
654                generated_line: m.generated_line,
655                generated_column: m.generated_column,
656                source: m.source.unwrap_or(u32::MAX),
657                original_line: m.original_line,
658                original_column: m.original_column,
659                name: m.name.unwrap_or(u32::MAX),
660                is_range_mapping: m.is_range_mapping,
661            })
662            .collect();
663
664        // Build sources_content: convert Vec<Option<String>> → Vec<Option<String>>
665        let sources_content: Vec<Option<String>> = self.sources_content.clone();
666
667        // Build the source root-prefixed sources (matching what from_json does)
668        let sources: Vec<String> = match &self.source_root {
669            Some(root) if !root.is_empty() => {
670                self.sources.iter().map(|s| format!("{root}{s}")).collect()
671            }
672            _ => self.sources.clone(),
673        };
674
675        srcmap_sourcemap::SourceMap::from_parts(
676            self.file.clone(),
677            self.source_root.clone(),
678            sources,
679            sources_content,
680            self.names.clone(),
681            sm_mappings,
682            self.ignore_list.clone(),
683            self.debug_id.clone(),
684            None, // scopes are not included in decoded map (would need encoding/decoding)
685        )
686    }
687}
688
689/// Source map generator that encodes VLQ on-the-fly.
690///
691/// Unlike [`SourceMapGenerator`], which collects all mappings and sorts them
692/// at finalization, `StreamingGenerator` encodes each mapping to VLQ immediately.
693/// Mappings **must** be added in sorted order `(generated_line, generated_column)`.
694///
695/// This avoids intermediate `Vec<Mapping>` allocation, making it ideal for
696/// streaming composition pipelines.
697///
698/// # Examples
699///
700/// ```rust
701/// use srcmap_generator::StreamingGenerator;
702///
703/// let mut sg = StreamingGenerator::new(Some("bundle.js".to_string()));
704/// let src = sg.add_source("src/app.ts");
705/// sg.set_source_content(src, "const x = 1;".to_string());
706///
707/// // Mappings must be added in order
708/// sg.add_mapping(0, 0, src, 0, 6);
709/// sg.add_mapping(1, 0, src, 1, 0);
710///
711/// let json = sg.to_json();
712/// assert!(json.contains(r#""version":3"#));
713/// ```
714#[derive(Debug)]
715pub struct StreamingGenerator {
716    file: Option<String>,
717    source_root: Option<String>,
718    sources: Vec<String>,
719    sources_content: Vec<Option<String>>,
720    names: Vec<String>,
721    ignore_list: Vec<u32>,
722    debug_id: Option<String>,
723
724    // Dedup maps
725    source_map: HashMap<String, u32>,
726    name_map: HashMap<String, u32>,
727
728    // Streaming VLQ state
729    vlq_out: Vec<u8>,
730    prev_gen_line: u32,
731    prev_gen_col: i64,
732    prev_source: i64,
733    prev_orig_line: i64,
734    prev_orig_col: i64,
735    prev_name: i64,
736    first_in_line: bool,
737    mapping_count: usize,
738
739    // Range mapping tracking
740    line_local_index: u32,
741    range_entries: Vec<(u32, u32)>,
742}
743
744impl StreamingGenerator {
745    /// Create a new streaming source map generator.
746    pub fn new(file: Option<String>) -> Self {
747        Self {
748            file,
749            source_root: None,
750            sources: Vec::new(),
751            sources_content: Vec::new(),
752            names: Vec::new(),
753            ignore_list: Vec::new(),
754            debug_id: None,
755            source_map: HashMap::new(),
756            name_map: HashMap::new(),
757            vlq_out: Vec::with_capacity(1024),
758            prev_gen_line: 0,
759            prev_gen_col: 0,
760            prev_source: 0,
761            prev_orig_line: 0,
762            prev_orig_col: 0,
763            prev_name: 0,
764            first_in_line: true,
765            mapping_count: 0,
766            line_local_index: 0,
767            range_entries: Vec::new(),
768        }
769    }
770
771    /// Set the source root prefix.
772    pub fn set_source_root(&mut self, root: impl Into<String>) {
773        self.source_root = Some(root.into());
774    }
775
776    /// Set the debug ID (UUID) for this source map (ECMA-426).
777    pub fn set_debug_id(&mut self, id: impl Into<String>) {
778        self.debug_id = Some(id.into());
779    }
780
781    /// Register a source file and return its index.
782    #[inline]
783    pub fn add_source(&mut self, source: &str) -> u32 {
784        if let Some(&idx) = self.source_map.get(source) {
785            return idx;
786        }
787        let idx = self.sources.len() as u32;
788        self.sources.push(source.to_string());
789        self.sources_content.push(None);
790        self.source_map.insert(source.to_string(), idx);
791        idx
792    }
793
794    /// Set the content for a source file.
795    pub fn set_source_content(&mut self, source_idx: u32, content: impl Into<String>) {
796        if (source_idx as usize) < self.sources_content.len() {
797            self.sources_content[source_idx as usize] = Some(content.into());
798        }
799    }
800
801    /// Register a name and return its index.
802    #[inline]
803    pub fn add_name(&mut self, name: &str) -> u32 {
804        if let Some(&idx) = self.name_map.get(name) {
805            return idx;
806        }
807        let idx = self.names.len() as u32;
808        self.names.push(name.to_string());
809        self.name_map.insert(name.to_string(), idx);
810        idx
811    }
812
813    /// Add a source index to the ignore list.
814    pub fn add_to_ignore_list(&mut self, source_idx: u32) {
815        if !self.ignore_list.contains(&source_idx) {
816            self.ignore_list.push(source_idx);
817        }
818    }
819
820    /// Add a mapping with no source information (generated-only).
821    ///
822    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
823    #[inline]
824    pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
825        self.advance_to_line(generated_line);
826
827        if !self.first_in_line {
828            self.vlq_out.push(b',');
829        }
830        self.first_in_line = false;
831
832        vlq_encode(
833            &mut self.vlq_out,
834            generated_column as i64 - self.prev_gen_col,
835        );
836        self.prev_gen_col = generated_column as i64;
837        self.line_local_index += 1;
838        self.mapping_count += 1;
839    }
840
841    /// Add a mapping from generated position to original position.
842    ///
843    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
844    #[inline]
845    pub fn add_mapping(
846        &mut self,
847        generated_line: u32,
848        generated_column: u32,
849        source: u32,
850        original_line: u32,
851        original_column: u32,
852    ) {
853        self.advance_to_line(generated_line);
854
855        if !self.first_in_line {
856            self.vlq_out.push(b',');
857        }
858        self.first_in_line = false;
859
860        vlq_encode(
861            &mut self.vlq_out,
862            generated_column as i64 - self.prev_gen_col,
863        );
864        self.prev_gen_col = generated_column as i64;
865
866        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
867        self.prev_source = source as i64;
868
869        vlq_encode(
870            &mut self.vlq_out,
871            original_line as i64 - self.prev_orig_line,
872        );
873        self.prev_orig_line = original_line as i64;
874
875        vlq_encode(
876            &mut self.vlq_out,
877            original_column as i64 - self.prev_orig_col,
878        );
879        self.prev_orig_col = original_column as i64;
880
881        self.line_local_index += 1;
882        self.mapping_count += 1;
883    }
884
885    /// Add a range mapping from generated position to original position.
886    ///
887    /// Same as [`add_mapping`](Self::add_mapping) but marks this mapping as a range mapping
888    /// (ECMA-426). Mappings must be added in sorted order `(generated_line, generated_column)`.
889    #[inline]
890    pub fn add_range_mapping(
891        &mut self,
892        generated_line: u32,
893        generated_column: u32,
894        source: u32,
895        original_line: u32,
896        original_column: u32,
897    ) {
898        self.advance_to_line(generated_line);
899        self.range_entries
900            .push((self.prev_gen_line, self.line_local_index));
901
902        if !self.first_in_line {
903            self.vlq_out.push(b',');
904        }
905        self.first_in_line = false;
906
907        vlq_encode(
908            &mut self.vlq_out,
909            generated_column as i64 - self.prev_gen_col,
910        );
911        self.prev_gen_col = generated_column as i64;
912
913        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
914        self.prev_source = source as i64;
915
916        vlq_encode(
917            &mut self.vlq_out,
918            original_line as i64 - self.prev_orig_line,
919        );
920        self.prev_orig_line = original_line as i64;
921
922        vlq_encode(
923            &mut self.vlq_out,
924            original_column as i64 - self.prev_orig_col,
925        );
926        self.prev_orig_col = original_column as i64;
927
928        self.line_local_index += 1;
929        self.mapping_count += 1;
930    }
931
932    /// Add a mapping with a name.
933    ///
934    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
935    #[inline]
936    pub fn add_named_mapping(
937        &mut self,
938        generated_line: u32,
939        generated_column: u32,
940        source: u32,
941        original_line: u32,
942        original_column: u32,
943        name: u32,
944    ) {
945        self.advance_to_line(generated_line);
946
947        if !self.first_in_line {
948            self.vlq_out.push(b',');
949        }
950        self.first_in_line = false;
951
952        vlq_encode(
953            &mut self.vlq_out,
954            generated_column as i64 - self.prev_gen_col,
955        );
956        self.prev_gen_col = generated_column as i64;
957
958        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
959        self.prev_source = source as i64;
960
961        vlq_encode(
962            &mut self.vlq_out,
963            original_line as i64 - self.prev_orig_line,
964        );
965        self.prev_orig_line = original_line as i64;
966
967        vlq_encode(
968            &mut self.vlq_out,
969            original_column as i64 - self.prev_orig_col,
970        );
971        self.prev_orig_col = original_column as i64;
972
973        vlq_encode(&mut self.vlq_out, name as i64 - self.prev_name);
974        self.prev_name = name as i64;
975
976        self.line_local_index += 1;
977        self.mapping_count += 1;
978    }
979
980    /// Add a named range mapping from generated position to original position.
981    ///
982    /// Same as [`add_named_mapping`](Self::add_named_mapping) but marks this mapping as a range
983    /// mapping (ECMA-426). Mappings must be added in sorted order
984    /// `(generated_line, generated_column)`.
985    #[inline]
986    pub fn add_named_range_mapping(
987        &mut self,
988        generated_line: u32,
989        generated_column: u32,
990        source: u32,
991        original_line: u32,
992        original_column: u32,
993        name: u32,
994    ) {
995        self.advance_to_line(generated_line);
996        self.range_entries
997            .push((self.prev_gen_line, self.line_local_index));
998
999        if !self.first_in_line {
1000            self.vlq_out.push(b',');
1001        }
1002        self.first_in_line = false;
1003
1004        vlq_encode(
1005            &mut self.vlq_out,
1006            generated_column as i64 - self.prev_gen_col,
1007        );
1008        self.prev_gen_col = generated_column as i64;
1009
1010        vlq_encode(&mut self.vlq_out, source as i64 - self.prev_source);
1011        self.prev_source = source as i64;
1012
1013        vlq_encode(
1014            &mut self.vlq_out,
1015            original_line as i64 - self.prev_orig_line,
1016        );
1017        self.prev_orig_line = original_line as i64;
1018
1019        vlq_encode(
1020            &mut self.vlq_out,
1021            original_column as i64 - self.prev_orig_col,
1022        );
1023        self.prev_orig_col = original_column as i64;
1024
1025        vlq_encode(&mut self.vlq_out, name as i64 - self.prev_name);
1026        self.prev_name = name as i64;
1027
1028        self.line_local_index += 1;
1029        self.mapping_count += 1;
1030    }
1031
1032    /// Get the number of mappings added so far.
1033    pub fn mapping_count(&self) -> usize {
1034        self.mapping_count
1035    }
1036
1037    /// Advance VLQ output to the given generated line, emitting semicolons.
1038    #[inline]
1039    fn advance_to_line(&mut self, generated_line: u32) {
1040        while self.prev_gen_line < generated_line {
1041            self.vlq_out.push(b';');
1042            self.prev_gen_line += 1;
1043            self.prev_gen_col = 0;
1044            self.first_in_line = true;
1045            self.line_local_index = 0;
1046        }
1047    }
1048
1049    /// Generate the source map as a JSON string.
1050    pub fn to_json(&self) -> String {
1051        use std::fmt::Write;
1052
1053        let vlq = self.vlq_string();
1054
1055        let mut json = String::with_capacity(256 + vlq.len());
1056        json.push_str(r#"{"version":3"#);
1057
1058        if let Some(ref file) = self.file {
1059            json.push_str(r#","file":"#);
1060            json_quote_into(&mut json, file);
1061        }
1062
1063        if let Some(ref root) = self.source_root {
1064            json.push_str(r#","sourceRoot":"#);
1065            json_quote_into(&mut json, root);
1066        }
1067
1068        json.push_str(r#","sources":["#);
1069        for (i, s) in self.sources.iter().enumerate() {
1070            if i > 0 {
1071                json.push(',');
1072            }
1073            json_quote_into(&mut json, s);
1074        }
1075        json.push(']');
1076
1077        if self.sources_content.iter().any(|c| c.is_some()) {
1078            json.push_str(r#","sourcesContent":["#);
1079            for (i, c) in self.sources_content.iter().enumerate() {
1080                if i > 0 {
1081                    json.push(',');
1082                }
1083                match c {
1084                    Some(content) => json_quote_into(&mut json, content),
1085                    None => json.push_str("null"),
1086                }
1087            }
1088            json.push(']');
1089        }
1090
1091        json.push_str(r#","names":["#);
1092        for (i, n) in self.names.iter().enumerate() {
1093            if i > 0 {
1094                json.push(',');
1095            }
1096            json_quote_into(&mut json, n);
1097        }
1098        json.push(']');
1099
1100        // VLQ string is pure base64/,/; — no escaping needed
1101        json.push_str(r#","mappings":""#);
1102        json.push_str(&vlq);
1103        json.push('"');
1104
1105        if !self.ignore_list.is_empty() {
1106            json.push_str(r#","ignoreList":["#);
1107            for (i, &idx) in self.ignore_list.iter().enumerate() {
1108                if i > 0 {
1109                    json.push(',');
1110                }
1111                let _ = write!(json, "{idx}");
1112            }
1113            json.push(']');
1114        }
1115
1116        if let Some(ref range_mappings) = self.encode_range_mappings() {
1117            json.push_str(r#","rangeMappings":""#);
1118            json.push_str(range_mappings);
1119            json.push('"');
1120        }
1121
1122        if let Some(ref id) = self.debug_id {
1123            json.push_str(r#","debugId":"#);
1124            json_quote_into(&mut json, id);
1125        }
1126
1127        json.push('}');
1128        json
1129    }
1130
1131    /// Directly construct a `SourceMap` from the streaming generator's state.
1132    ///
1133    /// Parses the already-encoded VLQ mappings to build a decoded `SourceMap`.
1134    /// More efficient than `to_json()` + `SourceMap::from_json()` since it
1135    /// skips JSON generation and parsing.
1136    ///
1137    /// # Panics
1138    ///
1139    /// Panics if the internal VLQ-encoded mappings string is corrupted or
1140    /// contains invalid VLQ sequences. This is not expected under normal use,
1141    /// since the streaming encoder always produces valid output.
1142    pub fn to_decoded_map(
1143        &self,
1144    ) -> Result<srcmap_sourcemap::SourceMap, srcmap_sourcemap::ParseError> {
1145        let vlq = self.vlq_string();
1146        let range_mappings = self.encode_range_mappings();
1147
1148        let sources: Vec<String> = match &self.source_root {
1149            Some(root) if !root.is_empty() => {
1150                self.sources.iter().map(|s| format!("{root}{s}")).collect()
1151            }
1152            _ => self.sources.clone(),
1153        };
1154
1155        srcmap_sourcemap::SourceMap::from_vlq_with_range_mappings(
1156            &vlq,
1157            sources,
1158            self.names.clone(),
1159            self.file.clone(),
1160            self.source_root.clone(),
1161            self.sources_content.clone(),
1162            self.ignore_list.clone(),
1163            self.debug_id.clone(),
1164            range_mappings.as_deref(),
1165        )
1166    }
1167
1168    /// Encode range mapping entries to a VLQ string.
1169    /// Returns `None` if no range mappings exist.
1170    fn encode_range_mappings(&self) -> Option<String> {
1171        if self.range_entries.is_empty() {
1172            return None;
1173        }
1174
1175        let max_line = self.range_entries.last().map_or(0, |&(line, _)| line);
1176        let mut out: Vec<u8> = Vec::new();
1177        let mut entry_idx = 0;
1178
1179        for line in 0..=max_line {
1180            if line > 0 {
1181                out.push(b';');
1182            }
1183            let mut prev_offset: u64 = 0;
1184            let mut first_on_line = true;
1185
1186            while entry_idx < self.range_entries.len() && self.range_entries[entry_idx].0 == line {
1187                if !first_on_line {
1188                    out.push(b',');
1189                }
1190                first_on_line = false;
1191                let local_idx = self.range_entries[entry_idx].1 as u64;
1192                let delta = local_idx - prev_offset;
1193                vlq_encode_unsigned(&mut out, delta);
1194                prev_offset = local_idx;
1195                entry_idx += 1;
1196            }
1197        }
1198
1199        while out.last() == Some(&b';') {
1200            out.pop();
1201        }
1202
1203        if out.is_empty() {
1204            return None;
1205        }
1206
1207        // SAFETY: VLQ output is always valid ASCII/UTF-8
1208        Some(unsafe { String::from_utf8_unchecked(out) })
1209    }
1210
1211    /// Get the VLQ mappings as a string, trimming trailing semicolons.
1212    fn vlq_string(&self) -> String {
1213        let end = self
1214            .vlq_out
1215            .iter()
1216            .rposition(|&b| b != b';')
1217            .map_or(0, |i| i + 1);
1218        // SAFETY: VLQ output is always valid ASCII/UTF-8
1219        unsafe { String::from_utf8_unchecked(self.vlq_out[..end].to_vec()) }
1220    }
1221}
1222
1223/// Encode a slice of mappings for a single line to VLQ bytes.
1224///
1225/// Generated column starts at 0 (reset per line).
1226/// Cumulative state is passed in from the sequential pre-scan.
1227#[cfg(feature = "parallel")]
1228fn encode_mapping_slice(
1229    mappings: &[&Mapping],
1230    init_source: i64,
1231    init_orig_line: i64,
1232    init_orig_col: i64,
1233    init_name: i64,
1234) -> Vec<u8> {
1235    let mut buf = Vec::with_capacity(mappings.len() * 6);
1236    let mut prev_gen_col: i64 = 0;
1237    let mut prev_source = init_source;
1238    let mut prev_orig_line = init_orig_line;
1239    let mut prev_orig_col = init_orig_col;
1240    let mut prev_name = init_name;
1241    let mut first = true;
1242
1243    for m in mappings {
1244        if !first {
1245            buf.push(b',');
1246        }
1247        first = false;
1248
1249        vlq_encode(&mut buf, m.generated_column as i64 - prev_gen_col);
1250        prev_gen_col = m.generated_column as i64;
1251
1252        if let Some(source) = m.source {
1253            vlq_encode(&mut buf, source as i64 - prev_source);
1254            prev_source = source as i64;
1255
1256            vlq_encode(&mut buf, m.original_line as i64 - prev_orig_line);
1257            prev_orig_line = m.original_line as i64;
1258
1259            vlq_encode(&mut buf, m.original_column as i64 - prev_orig_col);
1260            prev_orig_col = m.original_column as i64;
1261
1262            if let Some(name) = m.name {
1263                vlq_encode(&mut buf, name as i64 - prev_name);
1264                prev_name = name as i64;
1265            }
1266        }
1267    }
1268
1269    buf
1270}
1271
1272/// JSON-quote a string directly into the output (avoids intermediate allocation).
1273fn json_quote_into(out: &mut String, s: &str) {
1274    let bytes = s.as_bytes();
1275    out.push('"');
1276
1277    let mut start = 0;
1278    for (i, &b) in bytes.iter().enumerate() {
1279        let escape = match b {
1280            b'"' => "\\\"",
1281            b'\\' => "\\\\",
1282            b'\n' => "\\n",
1283            b'\r' => "\\r",
1284            b'\t' => "\\t",
1285            0x00..=0x1f => {
1286                if start < i {
1287                    out.push_str(&s[start..i]);
1288                }
1289                use std::fmt::Write;
1290                let _ = write!(out, "\\u{:04x}", b);
1291                start = i + 1;
1292                continue;
1293            }
1294            _ => continue,
1295        };
1296        if start < i {
1297            out.push_str(&s[start..i]);
1298        }
1299        out.push_str(escape);
1300        start = i + 1;
1301    }
1302
1303    if start < bytes.len() {
1304        out.push_str(&s[start..]);
1305    }
1306
1307    out.push('"');
1308}
1309
1310/// JSON-quote a string, returning a new String (used in parallel contexts).
1311#[cfg(feature = "parallel")]
1312fn json_quote(s: &str) -> String {
1313    let mut out = String::with_capacity(s.len() + 2);
1314    json_quote_into(&mut out, s);
1315    out
1316}
1317
1318// ── Tests ──────────────────────────────────────────────────────────
1319
1320#[cfg(test)]
1321mod tests {
1322    use super::*;
1323
1324    #[test]
1325    fn empty_generator() {
1326        let builder = SourceMapGenerator::new(None);
1327        let json = builder.to_json();
1328        assert!(json.contains(r#""version":3"#));
1329        assert!(json.contains(r#""mappings":"""#));
1330    }
1331
1332    #[test]
1333    fn simple_mapping() {
1334        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
1335        let src = builder.add_source("input.js");
1336        builder.add_mapping(0, 0, src, 0, 0);
1337
1338        let json = builder.to_json();
1339        assert!(json.contains(r#""file":"output.js""#));
1340        assert!(json.contains(r#""sources":["input.js"]"#));
1341
1342        // Verify roundtrip with parser
1343        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1344        let loc = sm.original_position_for(0, 0).unwrap();
1345        assert_eq!(sm.source(loc.source), "input.js");
1346        assert_eq!(loc.line, 0);
1347        assert_eq!(loc.column, 0);
1348    }
1349
1350    #[test]
1351    fn mapping_with_name() {
1352        let mut builder = SourceMapGenerator::new(None);
1353        let src = builder.add_source("input.js");
1354        let name = builder.add_name("myFunction");
1355        builder.add_named_mapping(0, 0, src, 0, 0, name);
1356
1357        let json = builder.to_json();
1358        assert!(json.contains(r#""names":["myFunction"]"#));
1359
1360        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1361        let loc = sm.original_position_for(0, 0).unwrap();
1362        assert_eq!(loc.name, Some(0));
1363        assert_eq!(sm.name(0), "myFunction");
1364    }
1365
1366    #[test]
1367    fn multiple_lines() {
1368        let mut builder = SourceMapGenerator::new(None);
1369        let src = builder.add_source("input.js");
1370        builder.add_mapping(0, 0, src, 0, 0);
1371        builder.add_mapping(1, 4, src, 1, 2);
1372        builder.add_mapping(2, 0, src, 2, 0);
1373
1374        let json = builder.to_json();
1375        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1376        assert_eq!(sm.line_count(), 3);
1377
1378        let loc = sm.original_position_for(1, 4).unwrap();
1379        assert_eq!(loc.line, 1);
1380        assert_eq!(loc.column, 2);
1381    }
1382
1383    #[test]
1384    fn multiple_sources() {
1385        let mut builder = SourceMapGenerator::new(None);
1386        let a = builder.add_source("a.js");
1387        let b = builder.add_source("b.js");
1388        builder.add_mapping(0, 0, a, 0, 0);
1389        builder.add_mapping(1, 0, b, 0, 0);
1390
1391        let json = builder.to_json();
1392        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1393
1394        let loc0 = sm.original_position_for(0, 0).unwrap();
1395        let loc1 = sm.original_position_for(1, 0).unwrap();
1396        assert_eq!(sm.source(loc0.source), "a.js");
1397        assert_eq!(sm.source(loc1.source), "b.js");
1398    }
1399
1400    #[test]
1401    fn source_content() {
1402        let mut builder = SourceMapGenerator::new(None);
1403        let src = builder.add_source("input.js");
1404        builder.set_source_content(src, "var x = 1;".to_string());
1405        builder.add_mapping(0, 0, src, 0, 0);
1406
1407        let json = builder.to_json();
1408        assert!(json.contains(r#""sourcesContent":["var x = 1;"]"#));
1409
1410        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1411        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
1412    }
1413
1414    #[test]
1415    fn source_root() {
1416        let mut builder = SourceMapGenerator::new(None);
1417        builder.set_source_root("src/".to_string());
1418        let src = builder.add_source("input.js");
1419        builder.add_mapping(0, 0, src, 0, 0);
1420
1421        let json = builder.to_json();
1422        assert!(json.contains(r#""sourceRoot":"src/""#));
1423    }
1424
1425    #[test]
1426    fn ignore_list() {
1427        let mut builder = SourceMapGenerator::new(None);
1428        let _app = builder.add_source("app.js");
1429        let lib = builder.add_source("node_modules/lib.js");
1430        builder.add_to_ignore_list(lib);
1431        builder.add_mapping(0, 0, lib, 0, 0);
1432
1433        let json = builder.to_json();
1434        assert!(json.contains(r#""ignoreList":[1]"#));
1435    }
1436
1437    #[test]
1438    fn generated_only_mapping() {
1439        let mut builder = SourceMapGenerator::new(None);
1440        builder.add_generated_mapping(0, 0);
1441
1442        let json = builder.to_json();
1443        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1444        // Generated-only mapping → no source info
1445        assert!(sm.original_position_for(0, 0).is_none());
1446    }
1447
1448    #[test]
1449    fn dedup_sources_and_names() {
1450        let mut builder = SourceMapGenerator::new(None);
1451        let s1 = builder.add_source("input.js");
1452        let s2 = builder.add_source("input.js"); // duplicate
1453        assert_eq!(s1, s2);
1454
1455        let n1 = builder.add_name("foo");
1456        let n2 = builder.add_name("foo"); // duplicate
1457        assert_eq!(n1, n2);
1458
1459        assert_eq!(builder.sources.len(), 1);
1460        assert_eq!(builder.names.len(), 1);
1461    }
1462
1463    #[test]
1464    fn large_roundtrip() {
1465        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1466
1467        for i in 0..5 {
1468            builder.add_source(&format!("src/file{i}.js"));
1469        }
1470        for i in 0..10 {
1471            builder.add_name(&format!("var{i}"));
1472        }
1473
1474        // Add 1000 mappings across 100 lines
1475        for line in 0..100u32 {
1476            for col in 0..10u32 {
1477                let src = (line * 10 + col) % 5;
1478                let name = if col % 3 == 0 { Some(col % 10) } else { None };
1479
1480                match name {
1481                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
1482                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
1483                }
1484            }
1485        }
1486
1487        let json = builder.to_json();
1488        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1489
1490        assert_eq!(sm.mapping_count(), 1000);
1491        assert_eq!(sm.line_count(), 100);
1492
1493        // Verify a few lookups
1494        let loc = sm.original_position_for(50, 30).unwrap();
1495        assert_eq!(loc.line, 50);
1496        assert_eq!(loc.column, 15);
1497    }
1498
1499    #[test]
1500    fn json_escaping() {
1501        let mut builder = SourceMapGenerator::new(None);
1502        let src = builder.add_source("path/with\"quotes.js");
1503        builder.set_source_content(src, "line1\nline2\ttab".to_string());
1504        builder.add_mapping(0, 0, src, 0, 0);
1505
1506        let json = builder.to_json();
1507        // Should be valid JSON
1508        let _: serde_json::Value = serde_json::from_str(&json).unwrap();
1509    }
1510
1511    #[test]
1512    fn maybe_add_mapping_skips_redundant() {
1513        let mut builder = SourceMapGenerator::new(None);
1514        let src = builder.add_source("input.js");
1515
1516        // First mapping — always added
1517        assert!(builder.maybe_add_mapping(0, 0, src, 10, 0));
1518        // Same source position, different generated column — redundant, skipped
1519        assert!(!builder.maybe_add_mapping(0, 5, src, 10, 0));
1520        // Different source position — added
1521        assert!(builder.maybe_add_mapping(0, 10, src, 11, 0));
1522        // Different generated line, same source position as last — added (new line resets)
1523        assert!(builder.maybe_add_mapping(1, 0, src, 11, 0));
1524
1525        assert_eq!(builder.mapping_count(), 3);
1526
1527        let json = builder.to_json();
1528        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1529        assert_eq!(sm.mapping_count(), 3);
1530    }
1531
1532    #[test]
1533    fn maybe_add_mapping_different_source() {
1534        let mut builder = SourceMapGenerator::new(None);
1535        let a = builder.add_source("a.js");
1536        let b = builder.add_source("b.js");
1537
1538        assert!(builder.maybe_add_mapping(0, 0, a, 0, 0));
1539        // Same line/col but different source — not redundant
1540        assert!(builder.maybe_add_mapping(0, 5, b, 0, 0));
1541
1542        assert_eq!(builder.mapping_count(), 2);
1543    }
1544
1545    #[test]
1546    fn to_decoded_map_basic() {
1547        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
1548        let src = builder.add_source("input.js");
1549        builder.add_mapping(0, 0, src, 0, 0);
1550        builder.add_mapping(1, 4, src, 1, 2);
1551
1552        let sm = builder.to_decoded_map();
1553        assert_eq!(sm.mapping_count(), 2);
1554        assert_eq!(sm.line_count(), 2);
1555
1556        let loc = sm.original_position_for(0, 0).unwrap();
1557        assert_eq!(sm.source(loc.source), "input.js");
1558        assert_eq!(loc.line, 0);
1559        assert_eq!(loc.column, 0);
1560
1561        let loc = sm.original_position_for(1, 4).unwrap();
1562        assert_eq!(loc.line, 1);
1563        assert_eq!(loc.column, 2);
1564    }
1565
1566    #[test]
1567    fn to_decoded_map_with_names() {
1568        let mut builder = SourceMapGenerator::new(None);
1569        let src = builder.add_source("input.js");
1570        let name = builder.add_name("myFunction");
1571        builder.add_named_mapping(0, 0, src, 0, 0, name);
1572
1573        let sm = builder.to_decoded_map();
1574        let loc = sm.original_position_for(0, 0).unwrap();
1575        assert_eq!(loc.name, Some(0));
1576        assert_eq!(sm.name(0), "myFunction");
1577    }
1578
1579    #[test]
1580    fn to_decoded_map_matches_json_roundtrip() {
1581        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1582        for i in 0..5 {
1583            builder.add_source(&format!("src/file{i}.js"));
1584        }
1585        for i in 0..10 {
1586            builder.add_name(&format!("var{i}"));
1587        }
1588
1589        for line in 0..50u32 {
1590            for col in 0..10u32 {
1591                let src = (line * 10 + col) % 5;
1592                let name = if col % 3 == 0 { Some(col % 10) } else { None };
1593                match name {
1594                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
1595                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
1596                }
1597            }
1598        }
1599
1600        // Compare decoded map vs JSON roundtrip
1601        let sm_decoded = builder.to_decoded_map();
1602        let json = builder.to_json();
1603        let sm_json = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1604
1605        assert_eq!(sm_decoded.mapping_count(), sm_json.mapping_count());
1606        assert_eq!(sm_decoded.line_count(), sm_json.line_count());
1607
1608        // Verify all lookups match
1609        for m in sm_json.all_mappings() {
1610            let a = sm_json.original_position_for(m.generated_line, m.generated_column);
1611            let b = sm_decoded.original_position_for(m.generated_line, m.generated_column);
1612            match (a, b) {
1613                (Some(a), Some(b)) => {
1614                    assert_eq!(
1615                        a.source, b.source,
1616                        "source mismatch at ({}, {})",
1617                        m.generated_line, m.generated_column
1618                    );
1619                    assert_eq!(
1620                        a.line, b.line,
1621                        "line mismatch at ({}, {})",
1622                        m.generated_line, m.generated_column
1623                    );
1624                    assert_eq!(
1625                        a.column, b.column,
1626                        "column mismatch at ({}, {})",
1627                        m.generated_line, m.generated_column
1628                    );
1629                    assert_eq!(
1630                        a.name, b.name,
1631                        "name mismatch at ({}, {})",
1632                        m.generated_line, m.generated_column
1633                    );
1634                }
1635                (None, None) => {}
1636                _ => panic!(
1637                    "lookup mismatch at ({}, {})",
1638                    m.generated_line, m.generated_column
1639                ),
1640            }
1641        }
1642    }
1643
1644    #[test]
1645    fn to_decoded_map_empty() {
1646        let builder = SourceMapGenerator::new(None);
1647        let sm = builder.to_decoded_map();
1648        assert_eq!(sm.mapping_count(), 0);
1649        assert_eq!(sm.line_count(), 0);
1650    }
1651
1652    #[test]
1653    fn to_decoded_map_generated_only() {
1654        let mut builder = SourceMapGenerator::new(None);
1655        builder.add_generated_mapping(0, 0);
1656
1657        let sm = builder.to_decoded_map();
1658        assert_eq!(sm.mapping_count(), 1);
1659        // Generated-only mapping has no source info
1660        assert!(sm.original_position_for(0, 0).is_none());
1661    }
1662
1663    #[test]
1664    fn to_decoded_map_multiple_sources() {
1665        let mut builder = SourceMapGenerator::new(None);
1666        let a = builder.add_source("a.js");
1667        let b = builder.add_source("b.js");
1668        builder.add_mapping(0, 0, a, 0, 0);
1669        builder.add_mapping(1, 0, b, 0, 0);
1670
1671        let sm = builder.to_decoded_map();
1672        let loc0 = sm.original_position_for(0, 0).unwrap();
1673        let loc1 = sm.original_position_for(1, 0).unwrap();
1674        assert_eq!(sm.source(loc0.source), "a.js");
1675        assert_eq!(sm.source(loc1.source), "b.js");
1676    }
1677
1678    #[test]
1679    fn to_decoded_map_with_source_content() {
1680        let mut builder = SourceMapGenerator::new(None);
1681        let src = builder.add_source("input.js");
1682        builder.set_source_content(src, "var x = 1;".to_string());
1683        builder.add_mapping(0, 0, src, 0, 0);
1684
1685        let sm = builder.to_decoded_map();
1686        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
1687    }
1688
1689    #[test]
1690    fn to_decoded_map_reverse_lookup() {
1691        let mut builder = SourceMapGenerator::new(None);
1692        let src = builder.add_source("input.js");
1693        builder.add_mapping(0, 0, src, 10, 5);
1694
1695        let sm = builder.to_decoded_map();
1696        let loc = sm.generated_position_for("input.js", 10, 5).unwrap();
1697        assert_eq!(loc.line, 0);
1698        assert_eq!(loc.column, 0);
1699    }
1700
1701    #[test]
1702    fn to_decoded_map_sparse_lines() {
1703        let mut builder = SourceMapGenerator::new(None);
1704        let src = builder.add_source("input.js");
1705        builder.add_mapping(0, 0, src, 0, 0);
1706        builder.add_mapping(5, 0, src, 5, 0);
1707
1708        let sm = builder.to_decoded_map();
1709        assert_eq!(sm.line_count(), 6);
1710        assert!(sm.original_position_for(0, 0).is_some());
1711        assert!(sm.original_position_for(2, 0).is_none());
1712        assert!(sm.original_position_for(5, 0).is_some());
1713    }
1714
1715    #[test]
1716    fn empty_lines_between_mappings() {
1717        let mut builder = SourceMapGenerator::new(None);
1718        let src = builder.add_source("input.js");
1719        builder.add_mapping(0, 0, src, 0, 0);
1720        // Skip lines 1-4
1721        builder.add_mapping(5, 0, src, 5, 0);
1722
1723        let json = builder.to_json();
1724        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1725
1726        // Line 0 should have a mapping
1727        assert!(sm.original_position_for(0, 0).is_some());
1728        // Lines 1-4 should have no mappings
1729        assert!(sm.original_position_for(2, 0).is_none());
1730        // Line 5 should have a mapping
1731        assert!(sm.original_position_for(5, 0).is_some());
1732    }
1733
1734    #[test]
1735    fn debug_id() {
1736        let mut builder = SourceMapGenerator::new(None);
1737        builder.set_debug_id("85314830-023f-4cf1-a267-535f4e37bb17".to_string());
1738        let src = builder.add_source("input.js");
1739        builder.add_mapping(0, 0, src, 0, 0);
1740
1741        let json = builder.to_json();
1742        assert!(json.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
1743
1744        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1745        assert_eq!(
1746            sm.debug_id.as_deref(),
1747            Some("85314830-023f-4cf1-a267-535f4e37bb17")
1748        );
1749    }
1750
1751    #[test]
1752    fn scopes_roundtrip() {
1753        use srcmap_scopes::{Binding, GeneratedRange, OriginalScope, Position, ScopeInfo};
1754
1755        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1756        let src = builder.add_source("input.js");
1757        builder.set_source_content(
1758            src,
1759            "function hello(name) {\n  return name;\n}\nhello('world');".to_string(),
1760        );
1761        let name_hello = builder.add_name("hello");
1762        builder.add_named_mapping(0, 0, src, 0, 0, name_hello);
1763        builder.add_mapping(1, 0, src, 1, 0);
1764
1765        // Set scopes
1766        builder.set_scopes(ScopeInfo {
1767            scopes: vec![Some(OriginalScope {
1768                start: Position { line: 0, column: 0 },
1769                end: Position {
1770                    line: 3,
1771                    column: 14,
1772                },
1773                name: None,
1774                kind: Some("global".to_string()),
1775                is_stack_frame: false,
1776                variables: vec!["hello".to_string()],
1777                children: vec![OriginalScope {
1778                    start: Position { line: 0, column: 9 },
1779                    end: Position { line: 2, column: 1 },
1780                    name: Some("hello".to_string()),
1781                    kind: Some("function".to_string()),
1782                    is_stack_frame: true,
1783                    variables: vec!["name".to_string()],
1784                    children: vec![],
1785                }],
1786            })],
1787            ranges: vec![GeneratedRange {
1788                start: Position { line: 0, column: 0 },
1789                end: Position {
1790                    line: 3,
1791                    column: 14,
1792                },
1793                is_stack_frame: false,
1794                is_hidden: false,
1795                definition: Some(0),
1796                call_site: None,
1797                bindings: vec![Binding::Expression("hello".to_string())],
1798                children: vec![GeneratedRange {
1799                    start: Position { line: 0, column: 9 },
1800                    end: Position { line: 2, column: 1 },
1801                    is_stack_frame: true,
1802                    is_hidden: false,
1803                    definition: Some(1),
1804                    call_site: None,
1805                    bindings: vec![Binding::Expression("name".to_string())],
1806                    children: vec![],
1807                }],
1808            }],
1809        });
1810
1811        let json = builder.to_json();
1812
1813        // Verify scopes field is present
1814        assert!(json.contains(r#""scopes":"#));
1815
1816        // Parse back and verify
1817        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1818        assert!(sm.scopes.is_some());
1819
1820        let scopes_info = sm.scopes.unwrap();
1821
1822        // Verify original scopes
1823        assert_eq!(scopes_info.scopes.len(), 1);
1824        let root_scope = scopes_info.scopes[0].as_ref().unwrap();
1825        assert_eq!(root_scope.kind.as_deref(), Some("global"));
1826        assert_eq!(root_scope.variables, vec!["hello"]);
1827        assert_eq!(root_scope.children.len(), 1);
1828
1829        let fn_scope = &root_scope.children[0];
1830        assert_eq!(fn_scope.name.as_deref(), Some("hello"));
1831        assert_eq!(fn_scope.kind.as_deref(), Some("function"));
1832        assert!(fn_scope.is_stack_frame);
1833        assert_eq!(fn_scope.variables, vec!["name"]);
1834
1835        // Verify generated ranges
1836        assert_eq!(scopes_info.ranges.len(), 1);
1837        let outer = &scopes_info.ranges[0];
1838        assert_eq!(outer.definition, Some(0));
1839        assert_eq!(
1840            outer.bindings,
1841            vec![Binding::Expression("hello".to_string())]
1842        );
1843        assert_eq!(outer.children.len(), 1);
1844
1845        let inner = &outer.children[0];
1846        assert_eq!(inner.definition, Some(1));
1847        assert!(inner.is_stack_frame);
1848        assert_eq!(
1849            inner.bindings,
1850            vec![Binding::Expression("name".to_string())]
1851        );
1852    }
1853
1854    #[test]
1855    fn scopes_with_inlining_roundtrip() {
1856        use srcmap_scopes::{
1857            Binding, CallSite, GeneratedRange, OriginalScope, Position, ScopeInfo,
1858        };
1859
1860        let mut builder = SourceMapGenerator::new(None);
1861        let src = builder.add_source("input.js");
1862        builder.add_mapping(0, 0, src, 0, 0);
1863
1864        builder.set_scopes(ScopeInfo {
1865            scopes: vec![Some(OriginalScope {
1866                start: Position { line: 0, column: 0 },
1867                end: Position {
1868                    line: 10,
1869                    column: 0,
1870                },
1871                name: None,
1872                kind: None,
1873                is_stack_frame: false,
1874                variables: vec!["x".to_string()],
1875                children: vec![OriginalScope {
1876                    start: Position { line: 1, column: 0 },
1877                    end: Position { line: 4, column: 1 },
1878                    name: Some("greet".to_string()),
1879                    kind: Some("function".to_string()),
1880                    is_stack_frame: true,
1881                    variables: vec!["msg".to_string()],
1882                    children: vec![],
1883                }],
1884            })],
1885            ranges: vec![GeneratedRange {
1886                start: Position { line: 0, column: 0 },
1887                end: Position {
1888                    line: 10,
1889                    column: 0,
1890                },
1891                is_stack_frame: false,
1892                is_hidden: false,
1893                definition: Some(0),
1894                call_site: None,
1895                bindings: vec![Binding::Expression("_x".to_string())],
1896                children: vec![GeneratedRange {
1897                    start: Position { line: 6, column: 0 },
1898                    end: Position { line: 8, column: 0 },
1899                    is_stack_frame: true,
1900                    is_hidden: false,
1901                    definition: Some(1),
1902                    call_site: Some(CallSite {
1903                        source_index: 0,
1904                        line: 8,
1905                        column: 0,
1906                    }),
1907                    bindings: vec![Binding::Expression("\"Hello\"".to_string())],
1908                    children: vec![],
1909                }],
1910            }],
1911        });
1912
1913        let json = builder.to_json();
1914        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1915        let info = sm.scopes.unwrap();
1916
1917        // Verify call site on inlined range
1918        let inlined = &info.ranges[0].children[0];
1919        assert_eq!(
1920            inlined.call_site,
1921            Some(CallSite {
1922                source_index: 0,
1923                line: 8,
1924                column: 0,
1925            })
1926        );
1927        assert_eq!(
1928            inlined.bindings,
1929            vec![Binding::Expression("\"Hello\"".to_string())]
1930        );
1931    }
1932
1933    #[test]
1934    fn set_source_content_out_of_bounds() {
1935        let mut builder = SourceMapGenerator::new(None);
1936        // No sources added, index 0 is out of bounds
1937        builder.set_source_content(0, "content".to_string());
1938        // Should silently do nothing
1939        let json = builder.to_json();
1940        assert!(!json.contains("content"));
1941    }
1942
1943    #[test]
1944    fn add_to_ignore_list_dedup() {
1945        let mut builder = SourceMapGenerator::new(None);
1946        let idx = builder.add_source("vendor.js");
1947        builder.add_to_ignore_list(idx);
1948        builder.add_to_ignore_list(idx); // duplicate - should be deduped
1949        let json = builder.to_json();
1950        // Should only appear once in ignoreList
1951        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1952        assert_eq!(sm.ignore_list, vec![0]);
1953    }
1954
1955    #[test]
1956    fn to_decoded_map_with_source_root() {
1957        let mut builder = SourceMapGenerator::new(None);
1958        builder.set_source_root("src/".to_string());
1959        let src = builder.add_source("app.ts");
1960        builder.add_mapping(0, 0, src, 0, 0);
1961        let sm = builder.to_decoded_map();
1962        // Sources should be prefixed with source_root
1963        assert_eq!(sm.sources, vec!["src/app.ts"]);
1964    }
1965
1966    #[test]
1967    fn json_escaping_special_chars() {
1968        let mut builder = SourceMapGenerator::new(None);
1969        let src = builder.add_source("a.js");
1970        // Content with special chars: quotes, backslash, newline, carriage return, tab, control char
1971        builder.set_source_content(src, "line1\nline2\r\ttab\\\"\x01end".to_string());
1972        builder.add_mapping(0, 0, src, 0, 0);
1973        let json = builder.to_json();
1974        // Verify it's valid JSON by parsing
1975        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1976        assert_eq!(
1977            sm.sources_content,
1978            vec![Some("line1\nline2\r\ttab\\\"\x01end".to_string())]
1979        );
1980    }
1981
1982    #[test]
1983    fn json_escaping_in_names() {
1984        let mut builder = SourceMapGenerator::new(None);
1985        let src = builder.add_source("a.js");
1986        let name = builder.add_name("func\"with\\special");
1987        builder.add_named_mapping(0, 0, src, 0, 0, name);
1988        let json = builder.to_json();
1989        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1990        assert_eq!(sm.names[0], "func\"with\\special");
1991    }
1992
1993    #[test]
1994    fn json_escaping_in_sources() {
1995        let mut builder = SourceMapGenerator::new(None);
1996        let src = builder.add_source("path/with\"quotes.js");
1997        builder.add_mapping(0, 0, src, 0, 0);
1998        let json = builder.to_json();
1999        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2000        assert_eq!(sm.sources[0], "path/with\"quotes.js");
2001    }
2002
2003    #[cfg(feature = "parallel")]
2004    mod parallel_tests {
2005        use super::*;
2006
2007        fn build_large_generator(lines: u32, cols_per_line: u32) -> SourceMapGenerator {
2008            let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
2009            for i in 0..10 {
2010                let src = builder.add_source(&format!("src/file{i}.js"));
2011                builder.set_source_content(
2012                    src,
2013                    format!("// source file {i}\n{}", "x = 1;\n".repeat(100)),
2014                );
2015            }
2016            for i in 0..20 {
2017                builder.add_name(&format!("var{i}"));
2018            }
2019
2020            for line in 0..lines {
2021                for col in 0..cols_per_line {
2022                    let src = (line * cols_per_line + col) % 10;
2023                    let name = if col % 3 == 0 { Some(col % 20) } else { None };
2024                    match name {
2025                        Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
2026                        None => builder.add_mapping(line, col * 10, src, line, col * 5),
2027                    }
2028                }
2029            }
2030            builder
2031        }
2032
2033        #[test]
2034        fn parallel_large_roundtrip() {
2035            let builder = build_large_generator(500, 20);
2036            let json = builder.to_json();
2037            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2038            assert_eq!(sm.mapping_count(), 10000);
2039            assert_eq!(sm.line_count(), 500);
2040
2041            // Verify lookups
2042            let loc = sm.original_position_for(250, 50).unwrap();
2043            assert_eq!(loc.line, 250);
2044            assert_eq!(loc.column, 25);
2045        }
2046
2047        #[test]
2048        fn parallel_matches_sequential() {
2049            let builder = build_large_generator(500, 20);
2050
2051            // Sort mappings the same way encode_mappings does
2052            let mut sorted: Vec<&Mapping> = builder.mappings.iter().collect();
2053            sorted.sort_unstable_by(|a, b| {
2054                a.generated_line
2055                    .cmp(&b.generated_line)
2056                    .then(a.generated_column.cmp(&b.generated_column))
2057            });
2058
2059            let sequential = SourceMapGenerator::encode_sequential_impl(&sorted);
2060            let parallel = SourceMapGenerator::encode_parallel_impl(&sorted);
2061            assert_eq!(sequential, parallel);
2062        }
2063
2064        #[test]
2065        fn parallel_with_sparse_lines() {
2066            let mut builder = SourceMapGenerator::new(None);
2067            let src = builder.add_source("input.js");
2068
2069            // Add mappings on lines 0, 100, 200, ... (sparse)
2070            for i in 0..50 {
2071                let line = i * 100;
2072                for col in 0..100u32 {
2073                    builder.add_mapping(line, col * 10, src, line, col * 5);
2074                }
2075            }
2076
2077            let json = builder.to_json();
2078            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2079            assert_eq!(sm.mapping_count(), 5000);
2080
2081            // Verify empty lines have no mappings
2082            assert!(sm.original_position_for(50, 0).is_none());
2083            // Verify populated lines work
2084            let loc = sm.original_position_for(200, 50).unwrap();
2085            assert_eq!(loc.line, 200);
2086            assert_eq!(loc.column, 25);
2087        }
2088    }
2089
2090    // ── StreamingGenerator tests ────────────────────────────────
2091
2092    #[test]
2093    fn streaming_basic() {
2094        let mut sg = StreamingGenerator::new(Some("out.js".to_string()));
2095        let src = sg.add_source("input.js");
2096        sg.add_mapping(0, 0, src, 0, 0);
2097        sg.add_mapping(1, 0, src, 1, 0);
2098
2099        let json = sg.to_json();
2100        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2101        assert_eq!(sm.sources, vec!["input.js"]);
2102        assert_eq!(sm.mapping_count(), 2);
2103
2104        let loc0 = sm.original_position_for(0, 0).unwrap();
2105        assert_eq!(sm.source(loc0.source), "input.js");
2106        assert_eq!(loc0.line, 0);
2107
2108        let loc1 = sm.original_position_for(1, 0).unwrap();
2109        assert_eq!(loc1.line, 1);
2110    }
2111
2112    #[test]
2113    fn streaming_with_names() {
2114        let mut sg = StreamingGenerator::new(None);
2115        let src = sg.add_source("a.js");
2116        let name = sg.add_name("foo");
2117        sg.add_named_mapping(0, 0, src, 0, 0, name);
2118
2119        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2120        let loc = sm.original_position_for(0, 0).unwrap();
2121        assert_eq!(loc.name, Some(0));
2122        assert_eq!(sm.name(0), "foo");
2123    }
2124
2125    #[test]
2126    fn streaming_generated_only() {
2127        let mut sg = StreamingGenerator::new(None);
2128        let src = sg.add_source("a.js");
2129        sg.add_generated_mapping(0, 0);
2130        sg.add_mapping(0, 5, src, 0, 0);
2131
2132        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2133        assert_eq!(sm.mapping_count(), 2);
2134        assert!(sm.original_position_for(0, 0).is_none());
2135        assert!(sm.original_position_for(0, 5).is_some());
2136    }
2137
2138    #[test]
2139    fn streaming_matches_regular_generator() {
2140        let mut regular = SourceMapGenerator::new(Some("out.js".to_string()));
2141        let mut streaming = StreamingGenerator::new(Some("out.js".to_string()));
2142
2143        let src_r = regular.add_source("a.js");
2144        let src_s = streaming.add_source("a.js");
2145
2146        let name_r = regular.add_name("hello");
2147        let name_s = streaming.add_name("hello");
2148
2149        regular.set_source_content(src_r, "var hello;".to_string());
2150        streaming.set_source_content(src_s, "var hello;".to_string());
2151
2152        regular.add_named_mapping(0, 0, src_r, 0, 0, name_r);
2153        streaming.add_named_mapping(0, 0, src_s, 0, 0, name_s);
2154
2155        regular.add_mapping(0, 10, src_r, 0, 4);
2156        streaming.add_mapping(0, 10, src_s, 0, 4);
2157
2158        regular.add_mapping(1, 0, src_r, 1, 0);
2159        streaming.add_mapping(1, 0, src_s, 1, 0);
2160
2161        let sm_r = srcmap_sourcemap::SourceMap::from_json(&regular.to_json()).unwrap();
2162        let sm_s = srcmap_sourcemap::SourceMap::from_json(&streaming.to_json()).unwrap();
2163
2164        assert_eq!(sm_r.mapping_count(), sm_s.mapping_count());
2165        assert_eq!(sm_r.sources, sm_s.sources);
2166        assert_eq!(sm_r.names, sm_s.names);
2167        assert_eq!(sm_r.sources_content, sm_s.sources_content);
2168
2169        for (a, b) in sm_r.all_mappings().iter().zip(sm_s.all_mappings().iter()) {
2170            assert_eq!(a.generated_line, b.generated_line);
2171            assert_eq!(a.generated_column, b.generated_column);
2172            assert_eq!(a.source, b.source);
2173            assert_eq!(a.original_line, b.original_line);
2174            assert_eq!(a.original_column, b.original_column);
2175            assert_eq!(a.name, b.name);
2176        }
2177    }
2178
2179    #[test]
2180    fn streaming_to_decoded_map() {
2181        let mut sg = StreamingGenerator::new(None);
2182        let src = sg.add_source("test.js");
2183        sg.add_mapping(0, 0, src, 0, 0);
2184        sg.add_mapping(2, 5, src, 1, 3);
2185
2186        let sm = sg.to_decoded_map().unwrap();
2187        assert_eq!(sm.mapping_count(), 2);
2188        assert_eq!(sm.sources, vec!["test.js"]);
2189
2190        let loc = sm.original_position_for(2, 5).unwrap();
2191        assert_eq!(loc.line, 1);
2192        assert_eq!(loc.column, 3);
2193    }
2194
2195    #[test]
2196    fn streaming_source_dedup() {
2197        let mut sg = StreamingGenerator::new(None);
2198        let src1 = sg.add_source("a.js");
2199        let src2 = sg.add_source("a.js");
2200        assert_eq!(src1, src2);
2201        assert_eq!(sg.sources.len(), 1);
2202    }
2203
2204    #[test]
2205    fn streaming_ignore_list() {
2206        let mut sg = StreamingGenerator::new(None);
2207        let src = sg.add_source("vendor.js");
2208        sg.add_to_ignore_list(src);
2209        sg.add_mapping(0, 0, src, 0, 0);
2210
2211        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2212        assert_eq!(sm.ignore_list, vec![0]);
2213    }
2214
2215    #[test]
2216    fn streaming_empty() {
2217        let sg = StreamingGenerator::new(None);
2218        let json = sg.to_json();
2219        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2220        assert_eq!(sm.mapping_count(), 0);
2221    }
2222
2223    #[test]
2224    fn streaming_sparse_lines() {
2225        let mut sg = StreamingGenerator::new(None);
2226        let src = sg.add_source("a.js");
2227        sg.add_mapping(0, 0, src, 0, 0);
2228        sg.add_mapping(5, 0, src, 5, 0);
2229
2230        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2231        assert_eq!(sm.mapping_count(), 2);
2232        assert!(sm.original_position_for(0, 0).is_some());
2233        assert!(sm.original_position_for(5, 0).is_some());
2234    }
2235
2236    // ── Range mapping tests ───────────────────────────────────
2237
2238    #[test]
2239    fn range_mapping_basic() {
2240        let mut builder = SourceMapGenerator::new(None);
2241        let src = builder.add_source("input.js");
2242        builder.add_range_mapping(0, 0, src, 0, 0);
2243        builder.add_mapping(0, 5, src, 0, 10);
2244
2245        let json = builder.to_json();
2246        assert!(json.contains(r#""rangeMappings":"A""#));
2247    }
2248
2249    #[test]
2250    fn range_mapping_multiple_on_line() {
2251        let mut builder = SourceMapGenerator::new(None);
2252        let src = builder.add_source("input.js");
2253        builder.add_range_mapping(0, 0, src, 0, 0);
2254        builder.add_mapping(0, 5, src, 0, 10);
2255        builder.add_range_mapping(0, 10, src, 0, 20);
2256
2257        let json = builder.to_json();
2258        assert!(json.contains(r#""rangeMappings":"A,C""#));
2259    }
2260
2261    #[test]
2262    fn range_mapping_multi_line() {
2263        let mut builder = SourceMapGenerator::new(None);
2264        let src = builder.add_source("input.js");
2265        builder.add_range_mapping(0, 0, src, 0, 0);
2266        builder.add_range_mapping(1, 0, src, 1, 0);
2267
2268        let json = builder.to_json();
2269        assert!(json.contains(r#""rangeMappings":"A;A""#));
2270    }
2271
2272    #[test]
2273    fn no_range_mappings_omits_field() {
2274        let mut builder = SourceMapGenerator::new(None);
2275        let src = builder.add_source("input.js");
2276        builder.add_mapping(0, 0, src, 0, 0);
2277
2278        let json = builder.to_json();
2279        assert!(!json.contains("rangeMappings"));
2280    }
2281
2282    #[test]
2283    fn named_range_mapping() {
2284        let mut builder = SourceMapGenerator::new(None);
2285        let src = builder.add_source("input.js");
2286        let name = builder.add_name("foo");
2287        builder.add_named_range_mapping(0, 0, src, 0, 0, name);
2288
2289        let json = builder.to_json();
2290        assert!(json.contains(r#""rangeMappings":"A""#));
2291    }
2292
2293    #[test]
2294    fn to_decoded_map_preserves_range_mappings() {
2295        let mut builder = SourceMapGenerator::new(None);
2296        let src = builder.add_source("input.js");
2297        builder.add_range_mapping(0, 0, src, 0, 0);
2298        builder.add_mapping(0, 5, src, 0, 10);
2299
2300        let sm = builder.to_decoded_map();
2301        assert!(sm.has_range_mappings());
2302        let mappings = sm.all_mappings();
2303        assert!(mappings[0].is_range_mapping);
2304        assert!(!mappings[1].is_range_mapping);
2305    }
2306
2307    // ── Streaming range mapping tests ────────────────────────────
2308
2309    #[test]
2310    fn streaming_range_mapping_basic() {
2311        let mut sg = StreamingGenerator::new(None);
2312        let src = sg.add_source("input.js");
2313        sg.add_range_mapping(0, 0, src, 0, 0);
2314        sg.add_mapping(0, 5, src, 0, 10);
2315
2316        let json = sg.to_json();
2317        assert!(json.contains(r#""rangeMappings":"A""#));
2318    }
2319
2320    #[test]
2321    fn streaming_range_mapping_roundtrip() {
2322        let mut sg = StreamingGenerator::new(None);
2323        let src = sg.add_source("input.js");
2324        sg.add_range_mapping(0, 0, src, 0, 0);
2325        sg.add_mapping(0, 5, src, 0, 10);
2326
2327        let sm = sg.to_decoded_map().unwrap();
2328        assert!(sm.has_range_mappings());
2329        let mappings = sm.all_mappings();
2330        assert!(mappings[0].is_range_mapping);
2331        assert!(!mappings[1].is_range_mapping);
2332    }
2333
2334    #[test]
2335    fn streaming_range_and_named_range() {
2336        let mut sg = StreamingGenerator::new(None);
2337        let src = sg.add_source("input.js");
2338        let name = sg.add_name("foo");
2339        sg.add_range_mapping(0, 0, src, 0, 0);
2340        sg.add_named_range_mapping(0, 10, src, 0, 5, name);
2341
2342        let json = sg.to_json();
2343        assert!(json.contains(r#""rangeMappings":"A,B""#));
2344
2345        let sm = sg.to_decoded_map().unwrap();
2346        assert!(sm.has_range_mappings());
2347        let mappings = sm.all_mappings();
2348        assert!(mappings[0].is_range_mapping);
2349        assert!(mappings[1].is_range_mapping);
2350    }
2351
2352    #[test]
2353    fn streaming_range_mapping_matches_regular() {
2354        let mut regular = SourceMapGenerator::new(None);
2355        let mut streaming = StreamingGenerator::new(None);
2356
2357        let src_r = regular.add_source("input.js");
2358        let src_s = streaming.add_source("input.js");
2359
2360        regular.add_range_mapping(0, 0, src_r, 0, 0);
2361        streaming.add_range_mapping(0, 0, src_s, 0, 0);
2362
2363        regular.add_mapping(0, 5, src_r, 0, 10);
2364        streaming.add_mapping(0, 5, src_s, 0, 10);
2365
2366        regular.add_range_mapping(0, 10, src_r, 0, 20);
2367        streaming.add_range_mapping(0, 10, src_s, 0, 20);
2368
2369        regular.add_range_mapping(1, 0, src_r, 1, 0);
2370        streaming.add_range_mapping(1, 0, src_s, 1, 0);
2371
2372        let json_r = regular.to_json();
2373        let json_s = streaming.to_json();
2374
2375        let sm_r = srcmap_sourcemap::SourceMap::from_json(&json_r).unwrap();
2376        let sm_s = srcmap_sourcemap::SourceMap::from_json(&json_s).unwrap();
2377
2378        assert_eq!(sm_r.mapping_count(), sm_s.mapping_count());
2379
2380        for (a, b) in sm_r.all_mappings().iter().zip(sm_s.all_mappings().iter()) {
2381            assert_eq!(a.generated_line, b.generated_line);
2382            assert_eq!(a.generated_column, b.generated_column);
2383            assert_eq!(a.source, b.source);
2384            assert_eq!(a.original_line, b.original_line);
2385            assert_eq!(a.original_column, b.original_column);
2386            assert_eq!(a.name, b.name);
2387            assert_eq!(a.is_range_mapping, b.is_range_mapping);
2388        }
2389    }
2390}