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 rustc_hash::FxHashMap;
28use srcmap_codec::{vlq_encode_unchecked, vlq_encode_unsigned};
29
30use srcmap_scopes::ScopeInfo;
31
32use std::io;
33
34// ── Public types ───────────────────────────────────────────────────
35
36/// Decomposed source map parts for structured access without JSON serialization.
37///
38/// Returned by [`SourceMapGenerator::into_parts`] and [`StreamingGenerator::into_parts`].
39/// Useful when the caller needs direct access to individual fields (e.g., for NAPI
40/// bindings that pass each field separately) without paying the cost of full JSON
41/// serialization.
42#[derive(Debug, Clone)]
43pub struct SourceMapParts {
44    /// The generated file name, if set.
45    pub file: Option<String>,
46    /// VLQ-encoded mappings string.
47    pub mappings: String,
48    /// List of original source file paths.
49    pub sources: Vec<String>,
50    /// List of identifier names referenced by mappings.
51    pub names: Vec<String>,
52    /// Source content for each source (parallel to `sources`).
53    pub sources_content: Vec<Option<String>>,
54    /// Indices into `sources` that should be ignored by debuggers.
55    pub ignore_list: Vec<u32>,
56    /// Debug ID (UUID) for this source map (ECMA-426).
57    pub debug_id: Option<String>,
58    /// Source root prefix.
59    pub source_root: Option<String>,
60    /// VLQ-encoded range mappings string (ECMA-426 proposal), if any.
61    pub range_mappings: Option<String>,
62}
63
64/// A mapping from a generated position to an original position.
65///
66/// Used with [`SourceMapGenerator`] to define position relationships.
67/// All positions are 0-based.
68#[derive(Debug, Clone)]
69pub struct Mapping {
70    /// 0-based line in the generated output.
71    pub generated_line: u32,
72    /// 0-based column in the generated output.
73    pub generated_column: u32,
74    /// Source index from [`SourceMapGenerator::add_source`], or `None` for generated-only.
75    pub source: Option<u32>,
76    /// 0-based line in the original source.
77    pub original_line: u32,
78    /// 0-based column in the original source.
79    pub original_column: u32,
80    /// Name index from [`SourceMapGenerator::add_name`], or `None`.
81    pub name: Option<u32>,
82    /// Whether this mapping is a range mapping (ECMA-426 rangeMappings proposal).
83    pub is_range_mapping: bool,
84}
85
86/// Builder for creating source maps incrementally.
87///
88/// Register sources and names first (they return indices), then add mappings
89/// that reference those indices. Mappings should be added in generated-position
90/// order, though the builder does not require it.
91///
92/// Sources and names are automatically deduplicated.
93///
94/// # Workflow
95///
96/// 1. [`add_source`](Self::add_source) — register each original file
97/// 2. [`set_source_content`](Self::set_source_content) — optionally attach content
98/// 3. [`add_name`](Self::add_name) — register identifier names
99/// 4. [`add_mapping`](Self::add_mapping) / [`add_named_mapping`](Self::add_named_mapping) — add mappings
100/// 5. [`to_json`](Self::to_json) — serialize to JSON
101#[derive(Debug)]
102pub struct SourceMapGenerator {
103    file: Option<String>,
104    source_root: Option<String>,
105    sources: Vec<String>,
106    sources_content: Vec<Option<String>>,
107    names: Vec<String>,
108    mappings: Vec<Mapping>,
109    ignore_list: Vec<u32>,
110    debug_id: Option<String>,
111    scopes: Option<ScopeInfo>,
112    assume_sorted: bool,
113    // Tracks whether `mappings` is non-decreasing by (generated_line,
114    // generated_column), maintained incrementally on every push so the encode
115    // path can skip the O(n) `is_sorted_by_position` re-scan.
116    mappings_in_order: bool,
117
118    // Dedup maps for O(1) lookup
119    source_map: FxHashMap<String, u32>,
120    name_map: FxHashMap<String, u32>,
121}
122
123impl SourceMapGenerator {
124    /// Create a new empty source map generator.
125    pub fn new(file: Option<String>) -> Self {
126        Self {
127            file,
128            source_root: None,
129            sources: Vec::new(),
130            sources_content: Vec::new(),
131            names: Vec::new(),
132            mappings: Vec::new(),
133            ignore_list: Vec::new(),
134            debug_id: None,
135            scopes: None,
136            assume_sorted: false,
137            mappings_in_order: true,
138            source_map: FxHashMap::default(),
139            name_map: FxHashMap::default(),
140        }
141    }
142
143    /// Create a new source map generator with pre-allocated capacity.
144    ///
145    /// Pre-allocates the internal mappings vector, avoiding re-allocations
146    /// when the number of mappings is known upfront.
147    pub fn with_capacity(file: Option<String>, mapping_capacity: usize) -> Self {
148        Self {
149            file,
150            source_root: None,
151            sources: Vec::new(),
152            sources_content: Vec::new(),
153            names: Vec::new(),
154            mappings: Vec::with_capacity(mapping_capacity),
155            ignore_list: Vec::new(),
156            debug_id: None,
157            scopes: None,
158            assume_sorted: false,
159            mappings_in_order: true,
160            source_map: FxHashMap::default(),
161            name_map: FxHashMap::default(),
162        }
163    }
164
165    /// Set the source root prefix.
166    pub fn set_source_root(&mut self, root: impl Into<String>) {
167        self.source_root = Some(root.into());
168    }
169
170    /// Set the debug ID (UUID) for this source map (ECMA-426).
171    pub fn set_debug_id(&mut self, id: impl Into<String>) {
172        self.debug_id = Some(id.into());
173    }
174
175    /// Set scope and variable information (ECMA-426 scopes proposal).
176    pub fn set_scopes(&mut self, scopes: ScopeInfo) {
177        self.scopes = Some(scopes);
178    }
179
180    /// Assume mappings are already sorted by `(generated_line, generated_column)`.
181    ///
182    /// When set to `true`, the internal encoding path and [`to_decoded_map`](Self::to_decoded_map)
183    /// skip the sort step, avoiding
184    /// both the `O(n log n)` sort and the `Vec<&Mapping>` allocation.
185    /// Most bundlers add mappings in order, so this is a safe optimization
186    /// in the common case.
187    pub fn set_assume_sorted(&mut self, sorted: bool) {
188        self.assume_sorted = sorted;
189    }
190
191    /// Register a source file and return its index.
192    #[inline]
193    pub fn add_source(&mut self, source: &str) -> u32 {
194        if let Some(&idx) = self.source_map.get(source) {
195            return idx;
196        }
197        let idx = self.sources.len() as u32;
198        self.sources.push(source.to_string());
199        self.sources_content.push(None);
200        self.source_map.insert(source.to_string(), idx);
201        idx
202    }
203
204    /// Set the content for a source file.
205    pub fn set_source_content(&mut self, source_idx: u32, content: impl Into<String>) {
206        if (source_idx as usize) < self.sources_content.len() {
207            self.sources_content[source_idx as usize] = Some(content.into());
208        }
209    }
210
211    /// Register a name and return its index.
212    #[inline]
213    pub fn add_name(&mut self, name: &str) -> u32 {
214        if let Some(&idx) = self.name_map.get(name) {
215            return idx;
216        }
217        let idx = self.names.len() as u32;
218        self.names.push(name.to_string());
219        self.name_map.insert(name.to_string(), idx);
220        idx
221    }
222
223    /// Add a source index to the ignore list.
224    pub fn add_to_ignore_list(&mut self, source_idx: u32) {
225        if !self.ignore_list.contains(&source_idx) {
226            self.ignore_list.push(source_idx);
227        }
228    }
229
230    /// Add a mapping with no source information (generated-only).
231    /// Push a mapping, maintaining the incremental `mappings_in_order` flag.
232    ///
233    /// Single choke point for every mapping insertion: keeping the
234    /// non-decreasing-position invariant updated here lets the encode path
235    /// avoid an O(n) sortedness scan. Mirrors `is_sorted_by_position`'s
236    /// `(generated_line, generated_column)` ordering exactly.
237    #[inline]
238    fn push_mapping(&mut self, mapping: Mapping) {
239        if self.mappings_in_order
240            && let Some(last) = self.mappings.last()
241            && (mapping.generated_line, mapping.generated_column)
242                < (last.generated_line, last.generated_column)
243        {
244            self.mappings_in_order = false;
245        }
246        self.mappings.push(mapping);
247    }
248
249    pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
250        self.push_mapping(Mapping {
251            generated_line,
252            generated_column,
253            source: None,
254            original_line: 0,
255            original_column: 0,
256            name: None,
257            is_range_mapping: false,
258        });
259    }
260
261    /// Add a mapping from generated position to original position.
262    pub fn add_mapping(
263        &mut self,
264        generated_line: u32,
265        generated_column: u32,
266        source: u32,
267        original_line: u32,
268        original_column: u32,
269    ) {
270        self.push_mapping(Mapping {
271            generated_line,
272            generated_column,
273            source: Some(source),
274            original_line,
275            original_column,
276            name: None,
277            is_range_mapping: false,
278        });
279    }
280
281    /// Add a mapping with a name.
282    pub fn add_named_mapping(
283        &mut self,
284        generated_line: u32,
285        generated_column: u32,
286        source: u32,
287        original_line: u32,
288        original_column: u32,
289        name: u32,
290    ) {
291        self.push_mapping(Mapping {
292            generated_line,
293            generated_column,
294            source: Some(source),
295            original_line,
296            original_column,
297            name: Some(name),
298            is_range_mapping: false,
299        });
300    }
301
302    /// Add a range mapping from generated position to original position.
303    ///
304    /// A range mapping maps every position from its generated position up to
305    /// (but not including) the next mapping, applying a proportional delta
306    /// to the original position (ECMA-426 `rangeMappings` proposal).
307    pub fn add_range_mapping(
308        &mut self,
309        generated_line: u32,
310        generated_column: u32,
311        source: u32,
312        original_line: u32,
313        original_column: u32,
314    ) {
315        self.push_mapping(Mapping {
316            generated_line,
317            generated_column,
318            source: Some(source),
319            original_line,
320            original_column,
321            name: None,
322            is_range_mapping: true,
323        });
324    }
325
326    /// Add a named range mapping.
327    pub fn add_named_range_mapping(
328        &mut self,
329        generated_line: u32,
330        generated_column: u32,
331        source: u32,
332        original_line: u32,
333        original_column: u32,
334        name: u32,
335    ) {
336        self.push_mapping(Mapping {
337            generated_line,
338            generated_column,
339            source: Some(source),
340            original_line,
341            original_column,
342            name: Some(name),
343            is_range_mapping: true,
344        });
345    }
346
347    /// Add a mapping only if it differs from the previous mapping on the same line.
348    ///
349    /// This skips redundant mappings where the source position is identical
350    /// to the last mapping, which reduces output size without losing information.
351    /// Used by bundlers and minifiers to avoid bloating source maps.
352    pub fn maybe_add_mapping(
353        &mut self,
354        generated_line: u32,
355        generated_column: u32,
356        source: u32,
357        original_line: u32,
358        original_column: u32,
359    ) -> bool {
360        if let Some(last) = self.mappings.last()
361            && last.generated_line == generated_line
362            && last.source == Some(source)
363            && last.original_line == original_line
364            && last.original_column == original_column
365        {
366            return false;
367        }
368        self.add_mapping(generated_line, generated_column, source, original_line, original_column);
369        true
370    }
371
372    /// Encode all mappings directly into the provided byte buffer.
373    /// Avoids intermediate String allocation when writing to an existing buffer.
374    fn encode_mappings_into(&self, out: &mut Vec<u8>) {
375        if self.mappings.is_empty() {
376            return;
377        }
378
379        // Fast path: skip sort when mappings are already in order (common case
380        // for bundlers that emit mappings sequentially).
381        if self.assume_sorted || self.mappings_in_order {
382            #[cfg(feature = "parallel")]
383            if self.mappings.len() >= 4096 {
384                let refs: Vec<&Mapping> = self.mappings.iter().collect();
385                let encoded = Self::encode_parallel_impl(&refs);
386                out.extend_from_slice(encoded.as_bytes());
387                return;
388            }
389            Self::encode_sequential_into(&self.mappings, out);
390            return;
391        }
392
393        // Sort mappings by (generated_line, generated_column)
394        let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
395        sorted.sort_unstable_by(|a, b| {
396            a.generated_line
397                .cmp(&b.generated_line)
398                .then(a.generated_column.cmp(&b.generated_column))
399        });
400
401        #[cfg(feature = "parallel")]
402        if sorted.len() >= 4096 {
403            let encoded = Self::encode_parallel_impl(&sorted);
404            out.extend_from_slice(encoded.as_bytes());
405            return;
406        }
407
408        Self::encode_sequential_into(&sorted, out);
409    }
410
411    /// Encode all mappings to a VLQ-encoded string.
412    #[allow(dead_code, reason = "used by tests and the parallel encoding path")]
413    fn encode_mappings(&self) -> String {
414        if self.mappings.is_empty() {
415            return String::new();
416        }
417
418        // Fast path: skip sort when mappings are already in order (common case
419        // for bundlers that emit mappings sequentially).
420        if self.assume_sorted || self.mappings_in_order {
421            #[cfg(feature = "parallel")]
422            if self.mappings.len() >= 4096 {
423                let refs: Vec<&Mapping> = self.mappings.iter().collect();
424                return Self::encode_parallel_impl(&refs);
425            }
426            return Self::encode_sequential_impl(&self.mappings);
427        }
428
429        // Sort mappings by (generated_line, generated_column)
430        let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
431        sorted.sort_unstable_by(|a, b| {
432            a.generated_line
433                .cmp(&b.generated_line)
434                .then(a.generated_column.cmp(&b.generated_column))
435        });
436
437        #[cfg(feature = "parallel")]
438        if sorted.len() >= 4096 {
439            return Self::encode_parallel_impl(&sorted);
440        }
441
442        Self::encode_sequential_impl(&sorted)
443    }
444
445    /// Encode mappings directly into an existing byte buffer.
446    ///
447    /// Writes VLQ-encoded mappings into `out`. Pre-allocates capacity internally
448    /// based on mapping count and max generated line.
449    #[inline]
450    fn encode_sequential_into<T: std::borrow::Borrow<Mapping>>(sorted: &[T], out: &mut Vec<u8>) {
451        // Ensure enough capacity: 36 bytes per mapping + semicolons for line gaps
452        let max_line = sorted.last().map_or(0, |m| m.borrow().generated_line as usize);
453        out.reserve(sorted.len() * 36 + max_line + 1);
454
455        let mut prev_gen_col: i64 = 0;
456        let mut prev_source: i64 = 0;
457        let mut prev_orig_line: i64 = 0;
458        let mut prev_orig_col: i64 = 0;
459        let mut prev_name: i64 = 0;
460        let mut prev_gen_line: u32 = 0;
461        let mut first_in_line = true;
462
463        for item in sorted {
464            let m = item.borrow();
465            while prev_gen_line < m.generated_line {
466                out.push(b';');
467                prev_gen_line += 1;
468                prev_gen_col = 0;
469                first_in_line = true;
470            }
471
472            if !first_in_line {
473                out.push(b',');
474            }
475            first_in_line = false;
476
477            // SAFETY: buffer pre-allocated with 36 bytes per mapping + max_line
478            // semicolons. Each mapping writes at most 5 × 7 = 35 VLQ bytes
479            // plus 1 separator, and each line gap writes 1 semicolon.
480            // Total capacity = sorted.len() * 36 + max_line + 1 ≥ all writes.
481            unsafe {
482                vlq_encode_unchecked(out, m.generated_column as i64 - prev_gen_col);
483                prev_gen_col = m.generated_column as i64;
484
485                if let Some(source) = m.source {
486                    vlq_encode_unchecked(out, source as i64 - prev_source);
487                    prev_source = source as i64;
488
489                    vlq_encode_unchecked(out, m.original_line as i64 - prev_orig_line);
490                    prev_orig_line = m.original_line as i64;
491
492                    vlq_encode_unchecked(out, m.original_column as i64 - prev_orig_col);
493                    prev_orig_col = m.original_column as i64;
494
495                    if let Some(name) = m.name {
496                        vlq_encode_unchecked(out, name as i64 - prev_name);
497                        prev_name = name as i64;
498                    }
499                }
500            }
501        }
502    }
503
504    #[inline]
505    #[allow(dead_code, reason = "used by tests and the parallel encoding path")]
506    fn encode_sequential_impl<T: std::borrow::Borrow<Mapping>>(sorted: &[T]) -> String {
507        let max_line = sorted.last().map_or(0, |m| m.borrow().generated_line as usize);
508        let mut out: Vec<u8> = Vec::with_capacity(sorted.len() * 36 + max_line + 1);
509        Self::encode_sequential_into(sorted, &mut out);
510
511        debug_assert!(out.is_ascii());
512        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
513        // and we only add b';' and b',' — all valid UTF-8.
514        unsafe { String::from_utf8_unchecked(out) }
515    }
516
517    #[cfg(feature = "parallel")]
518    fn encode_parallel_impl(sorted: &[&Mapping]) -> String {
519        use rayon::prelude::*;
520
521        let max_line = sorted
522            .last()
523            .expect("encode_parallel_impl requires non-empty sorted slice")
524            .generated_line as usize;
525
526        // Build line ranges: (start_idx, end_idx) into sorted slice
527        let mut line_ranges: Vec<(usize, usize)> = vec![(0, 0); max_line + 1];
528        let mut i = 0;
529        while i < sorted.len() {
530            let line = sorted[i].generated_line as usize;
531            let start = i;
532            while i < sorted.len() && sorted[i].generated_line as usize == line {
533                i += 1;
534            }
535            line_ranges[line] = (start, i);
536        }
537
538        // Sequential scan: compute cumulative state at each line boundary
539        let mut states: Vec<(i64, i64, i64, i64)> = Vec::with_capacity(max_line + 1);
540        let mut prev_source: i64 = 0;
541        let mut prev_orig_line: i64 = 0;
542        let mut prev_orig_col: i64 = 0;
543        let mut prev_name: i64 = 0;
544
545        for &(start, end) in &line_ranges {
546            states.push((prev_source, prev_orig_line, prev_orig_col, prev_name));
547            for m in &sorted[start..end] {
548                if let Some(source) = m.source {
549                    prev_source = source as i64;
550                    prev_orig_line = m.original_line as i64;
551                    prev_orig_col = m.original_column as i64;
552                    if let Some(name) = m.name {
553                        prev_name = name as i64;
554                    }
555                }
556            }
557        }
558
559        // Parallel: encode each line independently
560        let encoded_lines: Vec<Vec<u8>> = line_ranges
561            .par_iter()
562            .zip(states.par_iter())
563            .map(|(&(start, end), &(s, ol, oc, n))| {
564                if start == end {
565                    return Vec::new();
566                }
567                encode_mapping_slice(&sorted[start..end], s, ol, oc, n)
568            })
569            .collect();
570
571        // Join with semicolons
572        let total_len = encoded_lines.iter().map(|l| l.len()).sum::<usize>() + max_line;
573        let mut out: Vec<u8> = Vec::with_capacity(total_len);
574        for (i, bytes) in encoded_lines.iter().enumerate() {
575            if i > 0 {
576                out.push(b';');
577            }
578            out.extend_from_slice(bytes);
579        }
580
581        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
582        // and we only add b';' — all valid UTF-8.
583        debug_assert!(out.is_ascii());
584        unsafe { String::from_utf8_unchecked(out) }
585    }
586
587    /// Encode range mappings to a VLQ string.
588    /// Returns `None` if no range mappings exist.
589    fn encode_range_mappings(&self) -> Option<String> {
590        if !self.mappings.iter().any(|m| m.is_range_mapping) {
591            return None;
592        }
593
594        let ordered: Vec<&Mapping> = if self.assume_sorted || self.mappings_in_order
595        {
596            self.mappings.iter().collect()
597        } else {
598            let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
599            sorted.sort_unstable_by(|a, b| {
600                a.generated_line
601                    .cmp(&b.generated_line)
602                    .then(a.generated_column.cmp(&b.generated_column))
603            });
604            sorted
605        };
606
607        let max_line = ordered.last().map_or(0, |m| m.generated_line);
608        let mut out: Vec<u8> = Vec::new();
609        let mut ordered_idx = 0;
610
611        for line in 0..=max_line {
612            if line > 0 {
613                out.push(b';');
614            }
615            let mut prev_offset: u64 = 0;
616            let mut first_on_line = true;
617            let mut line_local_idx: u64 = 0;
618
619            while ordered_idx < ordered.len() && ordered[ordered_idx].generated_line == line {
620                if ordered[ordered_idx].is_range_mapping {
621                    if !first_on_line {
622                        out.push(b',');
623                    }
624                    first_on_line = false;
625                    let delta = line_local_idx - prev_offset;
626                    vlq_encode_unsigned(&mut out, delta);
627                    prev_offset = line_local_idx;
628                }
629                line_local_idx += 1;
630                ordered_idx += 1;
631            }
632        }
633
634        while out.last() == Some(&b';') {
635            out.pop();
636        }
637
638        if out.is_empty() {
639            return None;
640        }
641
642        debug_assert!(out.is_ascii());
643        // SAFETY: vlq_encode_unsigned only pushes ASCII base64 chars,
644        // and we only add b';' and b',' — all valid UTF-8.
645        Some(unsafe { String::from_utf8_unchecked(out) })
646    }
647
648    /// Generate the source map as a JSON string.
649    pub fn to_json(&self) -> String {
650        use std::io::Write;
651
652        // Encode scopes (may introduce names not yet in self.names)
653        let (scopes_str, names_for_json) = if let Some(ref scopes_info) = self.scopes {
654            let mut names = self.names.clone();
655            let s = srcmap_scopes::encode_scopes(scopes_info, &mut names);
656            (Some(s), std::borrow::Cow::Owned(names))
657        } else {
658            (None, std::borrow::Cow::Borrowed(&self.names))
659        };
660
661        // Estimate capacity for JSON output
662        let sources_size: usize = self.sources.iter().map(|s| s.len() + 4).sum();
663        let names_size: usize = names_for_json.iter().map(|n| n.len() + 4).sum();
664        let content_size: usize =
665            self.sources_content.iter().map(|c| c.as_ref().map_or(5, |s| s.len() + 4)).sum();
666        // Estimate mapping size: ~6 bytes per mapping on average
667        let mappings_estimate = self.mappings.len() * 6;
668        let capacity = 100 + sources_size + names_size + mappings_estimate + content_size;
669
670        let mut json: Vec<u8> = Vec::with_capacity(capacity);
671        json.extend_from_slice(br#"{"version":3"#);
672
673        if let Some(ref file) = self.file {
674            json.extend_from_slice(br#","file":"#);
675            json_quote_into(&mut json, file);
676        }
677
678        if let Some(ref root) = self.source_root {
679            json.extend_from_slice(br#","sourceRoot":"#);
680            json_quote_into(&mut json, root);
681        }
682
683        // sources
684        json.extend_from_slice(br#","sources":["#);
685        for (i, s) in self.sources.iter().enumerate() {
686            if i > 0 {
687                json.push(b',');
688            }
689            json_quote_into(&mut json, s);
690        }
691        json.push(b']');
692
693        // sourcesContent (only if any content is set)
694        if self.sources_content.iter().any(|c| c.is_some()) {
695            json.extend_from_slice(br#","sourcesContent":["#);
696
697            #[cfg(feature = "parallel")]
698            {
699                use rayon::prelude::*;
700
701                let total_content: usize =
702                    self.sources_content.iter().map(|c| c.as_ref().map_or(0, |s| s.len())).sum();
703
704                if self.sources_content.len() >= 8 && total_content >= 8192 {
705                    let quoted: Vec<String> = self
706                        .sources_content
707                        .par_iter()
708                        .map(|c| match c {
709                            Some(content) => json_quote(content),
710                            None => "null".to_string(),
711                        })
712                        .collect();
713                    for (i, q) in quoted.iter().enumerate() {
714                        if i > 0 {
715                            json.push(b',');
716                        }
717                        json.extend_from_slice(q.as_bytes());
718                    }
719                } else {
720                    for (i, c) in self.sources_content.iter().enumerate() {
721                        if i > 0 {
722                            json.push(b',');
723                        }
724                        match c {
725                            Some(content) => json_quote_into(&mut json, content),
726                            None => json.extend_from_slice(b"null"),
727                        }
728                    }
729                }
730            }
731
732            #[cfg(not(feature = "parallel"))]
733            for (i, c) in self.sources_content.iter().enumerate() {
734                if i > 0 {
735                    json.push(b',');
736                }
737                match c {
738                    Some(content) => json_quote_into(&mut json, content),
739                    None => json.extend_from_slice(b"null"),
740                }
741            }
742
743            json.push(b']');
744        }
745
746        // names
747        json.extend_from_slice(br#","names":["#);
748        for (i, n) in names_for_json.iter().enumerate() {
749            if i > 0 {
750                json.push(b',');
751            }
752            json_quote_into(&mut json, n);
753        }
754        json.push(b']');
755
756        // mappings — VLQ string is pure base64/,/; so no escaping needed.
757        // Write directly into the json buffer to avoid intermediate String allocation.
758        json.extend_from_slice(br#","mappings":""#);
759        self.encode_mappings_into(&mut json);
760        json.push(b'"');
761
762        // ignoreList
763        if !self.ignore_list.is_empty() {
764            json.extend_from_slice(br#","ignoreList":["#);
765            for (i, &idx) in self.ignore_list.iter().enumerate() {
766                if i > 0 {
767                    json.push(b',');
768                }
769                let _ = write!(json, "{idx}");
770            }
771            json.push(b']');
772        }
773
774        // rangeMappings (only if any range mappings exist)
775        if let Some(ref range_mappings) = self.encode_range_mappings() {
776            json.extend_from_slice(br#","rangeMappings":""#);
777            json.extend_from_slice(range_mappings.as_bytes());
778            json.push(b'"');
779        }
780
781        // debugId
782        if let Some(ref id) = self.debug_id {
783            json.extend_from_slice(br#","debugId":"#);
784            json_quote_into(&mut json, id);
785        }
786
787        // scopes (ECMA-426 scopes proposal)
788        if let Some(ref s) = scopes_str {
789            json.extend_from_slice(br#","scopes":"#);
790            json_quote_into(&mut json, s);
791        }
792
793        json.push(b'}');
794
795        // SAFETY: all content written is valid UTF-8 — ASCII JSON syntax, base64 VLQ,
796        // and original UTF-8 strings passed through json_quote_into.
797        unsafe { String::from_utf8_unchecked(json) }
798    }
799
800    /// Get the number of mappings.
801    pub fn mapping_count(&self) -> usize {
802        self.mappings.len()
803    }
804
805    /// Directly construct a `SourceMap` from the generator's internal state.
806    ///
807    /// This avoids the encode-then-decode round-trip (VLQ encode to JSON string,
808    /// then re-parse) that would otherwise be needed in composition pipelines.
809    pub fn to_decoded_map(&self) -> srcmap_sourcemap::SourceMap {
810        let convert_mapping = |m: &Mapping| srcmap_sourcemap::Mapping {
811            generated_line: m.generated_line,
812            generated_column: m.generated_column,
813            source: m.source.unwrap_or(u32::MAX),
814            original_line: m.original_line,
815            original_column: m.original_column,
816            name: m.name.unwrap_or(u32::MAX),
817            is_range_mapping: m.is_range_mapping,
818        };
819
820        let sm_mappings: Vec<srcmap_sourcemap::Mapping> =
821            if self.assume_sorted || self.mappings_in_order {
822                // Skip sort — iterate directly over the mappings vec
823                self.mappings.iter().map(convert_mapping).collect()
824            } else {
825                // Sort mappings by (generated_line, generated_column) — same as encode_mappings
826                let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
827                sorted.sort_unstable_by(|a, b| {
828                    a.generated_line
829                        .cmp(&b.generated_line)
830                        .then(a.generated_column.cmp(&b.generated_column))
831                });
832                sorted.iter().map(|m| convert_mapping(m)).collect()
833            };
834
835        // Build sources_content: convert Vec<Option<String>> → Vec<Option<String>>
836        let sources_content: Vec<Option<String>> = self.sources_content.clone();
837
838        // Build the source root-prefixed sources (matching what from_json does)
839        let sources: Vec<String> = match &self.source_root {
840            Some(root) if !root.is_empty() => {
841                self.sources.iter().map(|s| format!("{root}{s}")).collect()
842            }
843            _ => self.sources.clone(),
844        };
845
846        srcmap_sourcemap::SourceMap::from_parts(
847            self.file.clone(),
848            self.source_root.clone(),
849            sources,
850            sources_content,
851            self.names.clone(),
852            sm_mappings,
853            self.ignore_list.clone(),
854            self.debug_id.clone(),
855            None, // scopes are not included in decoded map (would need encoding/decoding)
856        )
857    }
858
859    /// Consume the generator and return decomposed source map parts.
860    ///
861    /// Returns a [`SourceMapParts`] struct with individual fields (file, mappings,
862    /// sources, names, etc.) without performing JSON serialization. Useful for
863    /// NAPI/WASM bindings that need structured access to each field.
864    pub fn into_parts(self) -> SourceMapParts {
865        let mappings = self.encode_mappings();
866        let range_mappings = self.encode_range_mappings();
867
868        SourceMapParts {
869            file: self.file,
870            mappings,
871            sources: self.sources,
872            names: self.names,
873            sources_content: self.sources_content,
874            ignore_list: self.ignore_list,
875            debug_id: self.debug_id,
876            source_root: self.source_root,
877            range_mappings,
878        }
879    }
880
881    /// Serialize the source map as JSON directly into a writer.
882    ///
883    /// Streams JSON output to the writer without building the full JSON string
884    /// in memory. Useful for writing directly to files or network streams.
885    pub fn to_writer(&self, writer: &mut impl io::Write) -> io::Result<()> {
886        // Encode scopes (may introduce names not yet in self.names)
887        let (scopes_str, names_for_json) = if let Some(ref scopes_info) = self.scopes {
888            let mut names = self.names.clone();
889            let s = srcmap_scopes::encode_scopes(scopes_info, &mut names);
890            (Some(s), std::borrow::Cow::Owned(names))
891        } else {
892            (None, std::borrow::Cow::Borrowed(&self.names))
893        };
894
895        writer.write_all(br#"{"version":3"#)?;
896
897        if let Some(ref file) = self.file {
898            writer.write_all(br#","file":"#)?;
899            write_json_quoted(writer, file)?;
900        }
901
902        if let Some(ref root) = self.source_root {
903            writer.write_all(br#","sourceRoot":"#)?;
904            write_json_quoted(writer, root)?;
905        }
906
907        // sources
908        writer.write_all(br#","sources":["#)?;
909        for (i, s) in self.sources.iter().enumerate() {
910            if i > 0 {
911                writer.write_all(b",")?;
912            }
913            write_json_quoted(writer, s)?;
914        }
915        writer.write_all(b"]")?;
916
917        // sourcesContent (only if any content is set)
918        if self.sources_content.iter().any(|c| c.is_some()) {
919            writer.write_all(br#","sourcesContent":["#)?;
920            for (i, c) in self.sources_content.iter().enumerate() {
921                if i > 0 {
922                    writer.write_all(b",")?;
923                }
924                match c {
925                    Some(content) => write_json_quoted(writer, content)?,
926                    None => writer.write_all(b"null")?,
927                }
928            }
929            writer.write_all(b"]")?;
930        }
931
932        // names
933        writer.write_all(br#","names":["#)?;
934        for (i, n) in names_for_json.iter().enumerate() {
935            if i > 0 {
936                writer.write_all(b",")?;
937            }
938            write_json_quoted(writer, n)?;
939        }
940        writer.write_all(b"]")?;
941
942        // mappings
943        writer.write_all(br#","mappings":""#)?;
944        let mut vlq_buf: Vec<u8> = Vec::new();
945        self.encode_mappings_into(&mut vlq_buf);
946        writer.write_all(&vlq_buf)?;
947        writer.write_all(b"\"")?;
948
949        // ignoreList
950        if !self.ignore_list.is_empty() {
951            writer.write_all(br#","ignoreList":["#)?;
952            for (i, &idx) in self.ignore_list.iter().enumerate() {
953                if i > 0 {
954                    writer.write_all(b",")?;
955                }
956                write!(writer, "{idx}")?;
957            }
958            writer.write_all(b"]")?;
959        }
960
961        // rangeMappings
962        if let Some(ref range_mappings) = self.encode_range_mappings() {
963            writer.write_all(br#","rangeMappings":""#)?;
964            writer.write_all(range_mappings.as_bytes())?;
965            writer.write_all(b"\"")?;
966        }
967
968        // debugId
969        if let Some(ref id) = self.debug_id {
970            writer.write_all(br#","debugId":"#)?;
971            write_json_quoted(writer, id)?;
972        }
973
974        // scopes
975        if let Some(ref s) = scopes_str {
976            writer.write_all(br#","scopes":"#)?;
977            write_json_quoted(writer, s)?;
978        }
979
980        writer.write_all(b"}")?;
981        Ok(())
982    }
983}
984
985/// Source map generator that encodes VLQ on-the-fly.
986///
987/// Unlike [`SourceMapGenerator`], which collects all mappings and sorts them
988/// at finalization, `StreamingGenerator` encodes each mapping to VLQ immediately.
989/// Mappings **must** be added in sorted order `(generated_line, generated_column)`.
990///
991/// This avoids intermediate `Vec<Mapping>` allocation, making it ideal for
992/// streaming composition pipelines.
993///
994/// # Examples
995///
996/// ```rust
997/// use srcmap_generator::StreamingGenerator;
998///
999/// let mut sg = StreamingGenerator::new(Some("bundle.js".to_string()));
1000/// let src = sg.add_source("src/app.ts");
1001/// sg.set_source_content(src, "const x = 1;".to_string());
1002///
1003/// // Mappings must be added in order
1004/// sg.add_mapping(0, 0, src, 0, 6);
1005/// sg.add_mapping(1, 0, src, 1, 0);
1006///
1007/// let json = sg.to_json();
1008/// assert!(json.contains(r#""version":3"#));
1009/// ```
1010#[derive(Debug)]
1011pub struct StreamingGenerator {
1012    file: Option<String>,
1013    source_root: Option<String>,
1014    sources: Vec<String>,
1015    sources_content: Vec<Option<String>>,
1016    names: Vec<String>,
1017    ignore_list: Vec<u32>,
1018    debug_id: Option<String>,
1019
1020    // Dedup maps
1021    source_map: FxHashMap<String, u32>,
1022    name_map: FxHashMap<String, u32>,
1023
1024    // Streaming VLQ state
1025    vlq_out: Vec<u8>,
1026    prev_gen_line: u32,
1027    prev_gen_col: i64,
1028    prev_source: i64,
1029    prev_orig_line: i64,
1030    prev_orig_col: i64,
1031    prev_name: i64,
1032    first_in_line: bool,
1033    mapping_count: usize,
1034
1035    // Range mapping tracking
1036    line_local_index: u32,
1037    range_entries: Vec<(u32, u32)>,
1038}
1039
1040impl StreamingGenerator {
1041    /// Create a new streaming source map generator.
1042    pub fn new(file: Option<String>) -> Self {
1043        Self {
1044            file,
1045            source_root: None,
1046            sources: Vec::new(),
1047            sources_content: Vec::new(),
1048            names: Vec::new(),
1049            ignore_list: Vec::new(),
1050            debug_id: None,
1051            source_map: FxHashMap::default(),
1052            name_map: FxHashMap::default(),
1053            vlq_out: Vec::with_capacity(1024),
1054            prev_gen_line: 0,
1055            prev_gen_col: 0,
1056            prev_source: 0,
1057            prev_orig_line: 0,
1058            prev_orig_col: 0,
1059            prev_name: 0,
1060            first_in_line: true,
1061            mapping_count: 0,
1062            line_local_index: 0,
1063            range_entries: Vec::new(),
1064        }
1065    }
1066
1067    /// Create a new streaming source map generator with pre-allocated VLQ capacity.
1068    pub fn with_capacity(file: Option<String>, vlq_capacity: usize) -> Self {
1069        Self {
1070            file,
1071            source_root: None,
1072            sources: Vec::new(),
1073            sources_content: Vec::new(),
1074            names: Vec::new(),
1075            ignore_list: Vec::new(),
1076            debug_id: None,
1077            source_map: FxHashMap::default(),
1078            name_map: FxHashMap::default(),
1079            vlq_out: Vec::with_capacity(vlq_capacity),
1080            prev_gen_line: 0,
1081            prev_gen_col: 0,
1082            prev_source: 0,
1083            prev_orig_line: 0,
1084            prev_orig_col: 0,
1085            prev_name: 0,
1086            first_in_line: true,
1087            mapping_count: 0,
1088            line_local_index: 0,
1089            range_entries: Vec::new(),
1090        }
1091    }
1092
1093    /// Set the source root prefix.
1094    pub fn set_source_root(&mut self, root: impl Into<String>) {
1095        self.source_root = Some(root.into());
1096    }
1097
1098    /// Set the debug ID (UUID) for this source map (ECMA-426).
1099    pub fn set_debug_id(&mut self, id: impl Into<String>) {
1100        self.debug_id = Some(id.into());
1101    }
1102
1103    /// Register a source file and return its index.
1104    #[inline]
1105    pub fn add_source(&mut self, source: &str) -> u32 {
1106        if let Some(&idx) = self.source_map.get(source) {
1107            return idx;
1108        }
1109        let idx = self.sources.len() as u32;
1110        self.sources.push(source.to_string());
1111        self.sources_content.push(None);
1112        self.source_map.insert(source.to_string(), idx);
1113        idx
1114    }
1115
1116    /// Set the content for a source file.
1117    pub fn set_source_content(&mut self, source_idx: u32, content: impl Into<String>) {
1118        if (source_idx as usize) < self.sources_content.len() {
1119            self.sources_content[source_idx as usize] = Some(content.into());
1120        }
1121    }
1122
1123    /// Register a name and return its index.
1124    #[inline]
1125    pub fn add_name(&mut self, name: &str) -> u32 {
1126        if let Some(&idx) = self.name_map.get(name) {
1127            return idx;
1128        }
1129        let idx = self.names.len() as u32;
1130        self.names.push(name.to_string());
1131        self.name_map.insert(name.to_string(), idx);
1132        idx
1133    }
1134
1135    /// Add a source index to the ignore list.
1136    pub fn add_to_ignore_list(&mut self, source_idx: u32) {
1137        if !self.ignore_list.contains(&source_idx) {
1138            self.ignore_list.push(source_idx);
1139        }
1140    }
1141
1142    /// Add a mapping with no source information (generated-only).
1143    ///
1144    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
1145    #[inline]
1146    pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
1147        self.advance_to_line(generated_line);
1148
1149        // Reserve enough for separator (1) + 1 VLQ value (7) = 8 bytes
1150        self.vlq_out.reserve(8);
1151
1152        if !self.first_in_line {
1153            self.vlq_out.push(b',');
1154        }
1155        self.first_in_line = false;
1156
1157        // SAFETY: we just reserved 8 bytes, vlq_encode_unchecked needs at most 7.
1158        unsafe {
1159            vlq_encode_unchecked(&mut self.vlq_out, generated_column as i64 - self.prev_gen_col);
1160        }
1161        self.prev_gen_col = generated_column as i64;
1162        self.line_local_index += 1;
1163        self.mapping_count += 1;
1164    }
1165
1166    /// Add a mapping from generated position to original position.
1167    ///
1168    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
1169    #[inline]
1170    pub fn add_mapping(
1171        &mut self,
1172        generated_line: u32,
1173        generated_column: u32,
1174        source: u32,
1175        original_line: u32,
1176        original_column: u32,
1177    ) {
1178        self.advance_to_line(generated_line);
1179
1180        // Reserve enough for separator (1) + 4 VLQ values (4 × 7 = 28) = 29 bytes
1181        self.vlq_out.reserve(29);
1182
1183        if !self.first_in_line {
1184            self.vlq_out.push(b',');
1185        }
1186        self.first_in_line = false;
1187
1188        // SAFETY: we reserved 29 bytes; 4 vlq_encode_unchecked calls use at most 28.
1189        unsafe {
1190            vlq_encode_unchecked(&mut self.vlq_out, generated_column as i64 - self.prev_gen_col);
1191            self.prev_gen_col = generated_column as i64;
1192
1193            vlq_encode_unchecked(&mut self.vlq_out, source as i64 - self.prev_source);
1194            self.prev_source = source as i64;
1195
1196            vlq_encode_unchecked(&mut self.vlq_out, original_line as i64 - self.prev_orig_line);
1197            self.prev_orig_line = original_line as i64;
1198
1199            vlq_encode_unchecked(&mut self.vlq_out, original_column as i64 - self.prev_orig_col);
1200            self.prev_orig_col = original_column as i64;
1201        }
1202
1203        self.line_local_index += 1;
1204        self.mapping_count += 1;
1205    }
1206
1207    /// Add a range mapping from generated position to original position.
1208    ///
1209    /// Same as [`add_mapping`](Self::add_mapping) but marks this mapping as a range mapping
1210    /// (ECMA-426). Mappings must be added in sorted order `(generated_line, generated_column)`.
1211    #[inline]
1212    pub fn add_range_mapping(
1213        &mut self,
1214        generated_line: u32,
1215        generated_column: u32,
1216        source: u32,
1217        original_line: u32,
1218        original_column: u32,
1219    ) {
1220        self.advance_to_line(generated_line);
1221        self.range_entries.push((self.prev_gen_line, self.line_local_index));
1222
1223        // Reserve enough for separator (1) + 4 VLQ values (4 × 7 = 28) = 29 bytes
1224        self.vlq_out.reserve(29);
1225
1226        if !self.first_in_line {
1227            self.vlq_out.push(b',');
1228        }
1229        self.first_in_line = false;
1230
1231        // SAFETY: we reserved 29 bytes; 4 vlq_encode_unchecked calls use at most 28.
1232        unsafe {
1233            vlq_encode_unchecked(&mut self.vlq_out, generated_column as i64 - self.prev_gen_col);
1234            self.prev_gen_col = generated_column as i64;
1235
1236            vlq_encode_unchecked(&mut self.vlq_out, source as i64 - self.prev_source);
1237            self.prev_source = source as i64;
1238
1239            vlq_encode_unchecked(&mut self.vlq_out, original_line as i64 - self.prev_orig_line);
1240            self.prev_orig_line = original_line as i64;
1241
1242            vlq_encode_unchecked(&mut self.vlq_out, original_column as i64 - self.prev_orig_col);
1243            self.prev_orig_col = original_column as i64;
1244        }
1245
1246        self.line_local_index += 1;
1247        self.mapping_count += 1;
1248    }
1249
1250    /// Add a mapping with a name.
1251    ///
1252    /// Mappings must be added in sorted order `(generated_line, generated_column)`.
1253    #[inline]
1254    pub fn add_named_mapping(
1255        &mut self,
1256        generated_line: u32,
1257        generated_column: u32,
1258        source: u32,
1259        original_line: u32,
1260        original_column: u32,
1261        name: u32,
1262    ) {
1263        self.advance_to_line(generated_line);
1264
1265        // Reserve enough for separator (1) + 5 VLQ values (5 × 7 = 35) = 36 bytes
1266        self.vlq_out.reserve(36);
1267
1268        if !self.first_in_line {
1269            self.vlq_out.push(b',');
1270        }
1271        self.first_in_line = false;
1272
1273        // SAFETY: we reserved 36 bytes; 5 vlq_encode_unchecked calls use at most 35.
1274        unsafe {
1275            vlq_encode_unchecked(&mut self.vlq_out, generated_column as i64 - self.prev_gen_col);
1276            self.prev_gen_col = generated_column as i64;
1277
1278            vlq_encode_unchecked(&mut self.vlq_out, source as i64 - self.prev_source);
1279            self.prev_source = source as i64;
1280
1281            vlq_encode_unchecked(&mut self.vlq_out, original_line as i64 - self.prev_orig_line);
1282            self.prev_orig_line = original_line as i64;
1283
1284            vlq_encode_unchecked(&mut self.vlq_out, original_column as i64 - self.prev_orig_col);
1285            self.prev_orig_col = original_column as i64;
1286
1287            vlq_encode_unchecked(&mut self.vlq_out, name as i64 - self.prev_name);
1288            self.prev_name = name as i64;
1289        }
1290
1291        self.line_local_index += 1;
1292        self.mapping_count += 1;
1293    }
1294
1295    /// Add a named range mapping from generated position to original position.
1296    ///
1297    /// Same as [`add_named_mapping`](Self::add_named_mapping) but marks this mapping as a range
1298    /// mapping (ECMA-426). Mappings must be added in sorted order
1299    /// `(generated_line, generated_column)`.
1300    #[inline]
1301    pub fn add_named_range_mapping(
1302        &mut self,
1303        generated_line: u32,
1304        generated_column: u32,
1305        source: u32,
1306        original_line: u32,
1307        original_column: u32,
1308        name: u32,
1309    ) {
1310        self.advance_to_line(generated_line);
1311        self.range_entries.push((self.prev_gen_line, self.line_local_index));
1312
1313        // Reserve enough for separator (1) + 5 VLQ values (5 × 7 = 35) = 36 bytes
1314        self.vlq_out.reserve(36);
1315
1316        if !self.first_in_line {
1317            self.vlq_out.push(b',');
1318        }
1319        self.first_in_line = false;
1320
1321        // SAFETY: we reserved 36 bytes; 5 vlq_encode_unchecked calls use at most 35.
1322        unsafe {
1323            vlq_encode_unchecked(&mut self.vlq_out, generated_column as i64 - self.prev_gen_col);
1324            self.prev_gen_col = generated_column as i64;
1325
1326            vlq_encode_unchecked(&mut self.vlq_out, source as i64 - self.prev_source);
1327            self.prev_source = source as i64;
1328
1329            vlq_encode_unchecked(&mut self.vlq_out, original_line as i64 - self.prev_orig_line);
1330            self.prev_orig_line = original_line as i64;
1331
1332            vlq_encode_unchecked(&mut self.vlq_out, original_column as i64 - self.prev_orig_col);
1333            self.prev_orig_col = original_column as i64;
1334
1335            vlq_encode_unchecked(&mut self.vlq_out, name as i64 - self.prev_name);
1336            self.prev_name = name as i64;
1337        }
1338
1339        self.line_local_index += 1;
1340        self.mapping_count += 1;
1341    }
1342
1343    /// Get the number of mappings added so far.
1344    pub fn mapping_count(&self) -> usize {
1345        self.mapping_count
1346    }
1347
1348    /// Advance VLQ output to the given generated line, emitting semicolons.
1349    #[inline]
1350    fn advance_to_line(&mut self, generated_line: u32) {
1351        while self.prev_gen_line < generated_line {
1352            self.vlq_out.push(b';');
1353            self.prev_gen_line += 1;
1354            self.prev_gen_col = 0;
1355            self.first_in_line = true;
1356            self.line_local_index = 0;
1357        }
1358    }
1359
1360    /// Generate the source map as a JSON string.
1361    pub fn to_json(&self) -> String {
1362        use std::io::Write;
1363
1364        let vlq = self.vlq_str();
1365
1366        // Better capacity estimate to avoid reallocs
1367        let sources_size: usize = self.sources.iter().map(|s| s.len() + 4).sum();
1368        let names_size: usize = self.names.iter().map(|n| n.len() + 4).sum();
1369        let content_size: usize =
1370            self.sources_content.iter().map(|c| c.as_ref().map_or(5, |s| s.len() + 4)).sum();
1371        let capacity = 100 + sources_size + names_size + vlq.len() + content_size;
1372
1373        let mut json: Vec<u8> = Vec::with_capacity(capacity);
1374        json.extend_from_slice(br#"{"version":3"#);
1375
1376        if let Some(ref file) = self.file {
1377            json.extend_from_slice(br#","file":"#);
1378            json_quote_into(&mut json, file);
1379        }
1380
1381        if let Some(ref root) = self.source_root {
1382            json.extend_from_slice(br#","sourceRoot":"#);
1383            json_quote_into(&mut json, root);
1384        }
1385
1386        json.extend_from_slice(br#","sources":["#);
1387        for (i, s) in self.sources.iter().enumerate() {
1388            if i > 0 {
1389                json.push(b',');
1390            }
1391            json_quote_into(&mut json, s);
1392        }
1393        json.push(b']');
1394
1395        if self.sources_content.iter().any(|c| c.is_some()) {
1396            json.extend_from_slice(br#","sourcesContent":["#);
1397            for (i, c) in self.sources_content.iter().enumerate() {
1398                if i > 0 {
1399                    json.push(b',');
1400                }
1401                match c {
1402                    Some(content) => json_quote_into(&mut json, content),
1403                    None => json.extend_from_slice(b"null"),
1404                }
1405            }
1406            json.push(b']');
1407        }
1408
1409        json.extend_from_slice(br#","names":["#);
1410        for (i, n) in self.names.iter().enumerate() {
1411            if i > 0 {
1412                json.push(b',');
1413            }
1414            json_quote_into(&mut json, n);
1415        }
1416        json.push(b']');
1417
1418        // VLQ string is pure base64/,/; — no escaping needed
1419        json.extend_from_slice(br#","mappings":""#);
1420        json.extend_from_slice(vlq.as_bytes());
1421        json.push(b'"');
1422
1423        if !self.ignore_list.is_empty() {
1424            json.extend_from_slice(br#","ignoreList":["#);
1425            for (i, &idx) in self.ignore_list.iter().enumerate() {
1426                if i > 0 {
1427                    json.push(b',');
1428                }
1429                let _ = write!(json, "{idx}");
1430            }
1431            json.push(b']');
1432        }
1433
1434        if let Some(ref range_mappings) = self.encode_range_mappings() {
1435            json.extend_from_slice(br#","rangeMappings":""#);
1436            json.extend_from_slice(range_mappings.as_bytes());
1437            json.push(b'"');
1438        }
1439
1440        if let Some(ref id) = self.debug_id {
1441            json.extend_from_slice(br#","debugId":"#);
1442            json_quote_into(&mut json, id);
1443        }
1444
1445        json.push(b'}');
1446
1447        // SAFETY: all content written is valid UTF-8 — ASCII JSON syntax, base64 VLQ,
1448        // and original UTF-8 strings passed through json_quote_into.
1449        unsafe { String::from_utf8_unchecked(json) }
1450    }
1451
1452    /// Directly construct a `SourceMap` from the streaming generator's state.
1453    ///
1454    /// Parses the already-encoded VLQ mappings to build a decoded `SourceMap`.
1455    /// More efficient than `to_json()` + `SourceMap::from_json()` since it
1456    /// skips JSON generation and parsing.
1457    ///
1458    /// # Panics
1459    ///
1460    /// Panics if the internal VLQ-encoded mappings string is corrupted or
1461    /// contains invalid VLQ sequences. This is not expected under normal use,
1462    /// since the streaming encoder always produces valid output.
1463    pub fn to_decoded_map(
1464        &self,
1465    ) -> Result<srcmap_sourcemap::SourceMap, srcmap_sourcemap::ParseError> {
1466        let vlq = self.vlq_str();
1467        let range_mappings = self.encode_range_mappings();
1468
1469        let sources: Vec<String> = match &self.source_root {
1470            Some(root) if !root.is_empty() => {
1471                self.sources.iter().map(|s| format!("{root}{s}")).collect()
1472            }
1473            _ => self.sources.clone(),
1474        };
1475
1476        srcmap_sourcemap::SourceMap::from_vlq_with_range_mappings(
1477            vlq,
1478            sources,
1479            self.names.clone(),
1480            self.file.clone(),
1481            self.source_root.clone(),
1482            self.sources_content.clone(),
1483            self.ignore_list.clone(),
1484            self.debug_id.clone(),
1485            range_mappings.as_deref(),
1486        )
1487    }
1488
1489    /// Encode range mapping entries to a VLQ string.
1490    /// Returns `None` if no range mappings exist.
1491    fn encode_range_mappings(&self) -> Option<String> {
1492        if self.range_entries.is_empty() {
1493            return None;
1494        }
1495
1496        let max_line = self.range_entries.last().map_or(0, |&(line, _)| line);
1497        let mut out: Vec<u8> = Vec::new();
1498        let mut entry_idx = 0;
1499
1500        for line in 0..=max_line {
1501            if line > 0 {
1502                out.push(b';');
1503            }
1504            let mut prev_offset: u64 = 0;
1505            let mut first_on_line = true;
1506
1507            while entry_idx < self.range_entries.len() && self.range_entries[entry_idx].0 == line {
1508                if !first_on_line {
1509                    out.push(b',');
1510                }
1511                first_on_line = false;
1512                let local_idx = self.range_entries[entry_idx].1 as u64;
1513                let delta = local_idx - prev_offset;
1514                vlq_encode_unsigned(&mut out, delta);
1515                prev_offset = local_idx;
1516                entry_idx += 1;
1517            }
1518        }
1519
1520        while out.last() == Some(&b';') {
1521            out.pop();
1522        }
1523
1524        if out.is_empty() {
1525            return None;
1526        }
1527
1528        // SAFETY: VLQ output is always valid ASCII/UTF-8.
1529        Some(unsafe { String::from_utf8_unchecked(out) })
1530    }
1531
1532    /// Get the VLQ mappings as a str slice, trimming trailing semicolons.
1533    fn vlq_str(&self) -> &str {
1534        let end = self.vlq_out.iter().rposition(|&b| b != b';').map_or(0, |i| i + 1);
1535        // SAFETY: VLQ output is always valid ASCII/UTF-8
1536        unsafe { std::str::from_utf8_unchecked(&self.vlq_out[..end]) }
1537    }
1538
1539    /// Consume the streaming generator and return decomposed source map parts.
1540    ///
1541    /// Returns a [`SourceMapParts`] struct with individual fields (file, mappings,
1542    /// sources, names, etc.) without performing JSON serialization. Useful for
1543    /// NAPI/WASM bindings that need structured access to each field.
1544    pub fn into_parts(self) -> SourceMapParts {
1545        let range_mappings = self.encode_range_mappings();
1546        // Trim trailing semicolons from VLQ output
1547        let end = self.vlq_out.iter().rposition(|&b| b != b';').map_or(0, |i| i + 1);
1548        // SAFETY: VLQ output is always valid ASCII/UTF-8
1549        let mappings = unsafe { String::from_utf8_unchecked(self.vlq_out[..end].to_vec()) };
1550
1551        SourceMapParts {
1552            file: self.file,
1553            mappings,
1554            sources: self.sources,
1555            names: self.names,
1556            sources_content: self.sources_content,
1557            ignore_list: self.ignore_list,
1558            debug_id: self.debug_id,
1559            source_root: self.source_root,
1560            range_mappings,
1561        }
1562    }
1563
1564    /// Serialize the source map as JSON directly into a writer.
1565    ///
1566    /// Streams JSON output to the writer without building the full JSON string
1567    /// in memory. Useful for writing directly to files or network streams.
1568    pub fn to_writer(&self, writer: &mut impl io::Write) -> io::Result<()> {
1569        let vlq = self.vlq_str();
1570
1571        writer.write_all(br#"{"version":3"#)?;
1572
1573        if let Some(ref file) = self.file {
1574            writer.write_all(br#","file":"#)?;
1575            write_json_quoted(writer, file)?;
1576        }
1577
1578        if let Some(ref root) = self.source_root {
1579            writer.write_all(br#","sourceRoot":"#)?;
1580            write_json_quoted(writer, root)?;
1581        }
1582
1583        // sources
1584        writer.write_all(br#","sources":["#)?;
1585        for (i, s) in self.sources.iter().enumerate() {
1586            if i > 0 {
1587                writer.write_all(b",")?;
1588            }
1589            write_json_quoted(writer, s)?;
1590        }
1591        writer.write_all(b"]")?;
1592
1593        // sourcesContent
1594        if self.sources_content.iter().any(|c| c.is_some()) {
1595            writer.write_all(br#","sourcesContent":["#)?;
1596            for (i, c) in self.sources_content.iter().enumerate() {
1597                if i > 0 {
1598                    writer.write_all(b",")?;
1599                }
1600                match c {
1601                    Some(content) => write_json_quoted(writer, content)?,
1602                    None => writer.write_all(b"null")?,
1603                }
1604            }
1605            writer.write_all(b"]")?;
1606        }
1607
1608        // names
1609        writer.write_all(br#","names":["#)?;
1610        for (i, n) in self.names.iter().enumerate() {
1611            if i > 0 {
1612                writer.write_all(b",")?;
1613            }
1614            write_json_quoted(writer, n)?;
1615        }
1616        writer.write_all(b"]")?;
1617
1618        // mappings
1619        writer.write_all(br#","mappings":""#)?;
1620        writer.write_all(vlq.as_bytes())?;
1621        writer.write_all(b"\"")?;
1622
1623        // ignoreList
1624        if !self.ignore_list.is_empty() {
1625            writer.write_all(br#","ignoreList":["#)?;
1626            for (i, &idx) in self.ignore_list.iter().enumerate() {
1627                if i > 0 {
1628                    writer.write_all(b",")?;
1629                }
1630                write!(writer, "{idx}")?;
1631            }
1632            writer.write_all(b"]")?;
1633        }
1634
1635        // rangeMappings
1636        if let Some(ref range_mappings) = self.encode_range_mappings() {
1637            writer.write_all(br#","rangeMappings":""#)?;
1638            writer.write_all(range_mappings.as_bytes())?;
1639            writer.write_all(b"\"")?;
1640        }
1641
1642        // debugId
1643        if let Some(ref id) = self.debug_id {
1644            writer.write_all(br#","debugId":"#)?;
1645            write_json_quoted(writer, id)?;
1646        }
1647
1648        writer.write_all(b"}")?;
1649        Ok(())
1650    }
1651}
1652
1653/// Encode a slice of mappings for a single line to VLQ bytes.
1654///
1655/// Generated column starts at 0 (reset per line).
1656/// Cumulative state is passed in from the sequential pre-scan.
1657#[cfg(feature = "parallel")]
1658fn encode_mapping_slice(
1659    mappings: &[&Mapping],
1660    init_source: i64,
1661    init_orig_line: i64,
1662    init_orig_col: i64,
1663    init_name: i64,
1664) -> Vec<u8> {
1665    // Pre-allocate: 36 bytes per mapping (5 VLQ × 7 + 1 separator)
1666    let mut buf = Vec::with_capacity(mappings.len() * 36);
1667    let mut prev_gen_col: i64 = 0;
1668    let mut prev_source = init_source;
1669    let mut prev_orig_line = init_orig_line;
1670    let mut prev_orig_col = init_orig_col;
1671    let mut prev_name = init_name;
1672    let mut first = true;
1673
1674    for m in mappings {
1675        if !first {
1676            buf.push(b',');
1677        }
1678        first = false;
1679
1680        // SAFETY: buffer pre-allocated with 36 bytes per mapping.
1681        // Each iteration writes at most 5 × 7 = 35 VLQ bytes + 1 separator.
1682        unsafe {
1683            vlq_encode_unchecked(&mut buf, m.generated_column as i64 - prev_gen_col);
1684            prev_gen_col = m.generated_column as i64;
1685
1686            if let Some(source) = m.source {
1687                vlq_encode_unchecked(&mut buf, source as i64 - prev_source);
1688                prev_source = source as i64;
1689
1690                vlq_encode_unchecked(&mut buf, m.original_line as i64 - prev_orig_line);
1691                prev_orig_line = m.original_line as i64;
1692
1693                vlq_encode_unchecked(&mut buf, m.original_column as i64 - prev_orig_col);
1694                prev_orig_col = m.original_column as i64;
1695
1696                if let Some(name) = m.name {
1697                    vlq_encode_unchecked(&mut buf, name as i64 - prev_name);
1698                    prev_name = name as i64;
1699                }
1700            }
1701        }
1702    }
1703
1704    buf
1705}
1706
1707/// JSON-quote a string directly into a byte buffer (avoids UTF-8 validation overhead).
1708fn json_quote_into(out: &mut Vec<u8>, s: &str) {
1709    let bytes = s.as_bytes();
1710    out.push(b'"');
1711
1712    let mut start = 0;
1713    for (i, &b) in bytes.iter().enumerate() {
1714        let escape: &[u8] = match b {
1715            b'"' => b"\\\"",
1716            b'\\' => b"\\\\",
1717            b'\n' => b"\\n",
1718            b'\r' => b"\\r",
1719            b'\t' => b"\\t",
1720            0x00..=0x1f => {
1721                out.extend_from_slice(&bytes[start..i]);
1722                let hex = b"0123456789abcdef";
1723                out.extend_from_slice(&[
1724                    b'\\',
1725                    b'u',
1726                    b'0',
1727                    b'0',
1728                    hex[(b >> 4) as usize],
1729                    hex[(b & 0xf) as usize],
1730                ]);
1731                start = i + 1;
1732                continue;
1733            }
1734            _ => continue,
1735        };
1736        out.extend_from_slice(&bytes[start..i]);
1737        out.extend_from_slice(escape);
1738        start = i + 1;
1739    }
1740
1741    out.extend_from_slice(&bytes[start..]);
1742    out.push(b'"');
1743}
1744
1745/// JSON-quote a string, returning a new String (used in parallel contexts).
1746#[cfg(feature = "parallel")]
1747fn json_quote(s: &str) -> String {
1748    let mut out = Vec::with_capacity(s.len() + 2);
1749    json_quote_into(&mut out, s);
1750    // SAFETY: json_quote_into only writes valid UTF-8 (ASCII escapes + original UTF-8 content).
1751    unsafe { String::from_utf8_unchecked(out) }
1752}
1753
1754/// JSON-quote a string directly into a writer.
1755fn write_json_quoted(writer: &mut impl io::Write, s: &str) -> io::Result<()> {
1756    let mut buf = Vec::with_capacity(s.len() + 2);
1757    json_quote_into(&mut buf, s);
1758    writer.write_all(&buf)
1759}
1760
1761// ── Tests ──────────────────────────────────────────────────────────
1762
1763#[cfg(test)]
1764mod tests {
1765    use super::*;
1766
1767    #[test]
1768    fn empty_generator() {
1769        let builder = SourceMapGenerator::new(None);
1770        let json = builder.to_json();
1771        assert!(json.contains(r#""version":3"#));
1772        assert!(json.contains(r#""mappings":"""#));
1773    }
1774
1775    #[test]
1776    fn simple_mapping() {
1777        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
1778        let src = builder.add_source("input.js");
1779        builder.add_mapping(0, 0, src, 0, 0);
1780
1781        let json = builder.to_json();
1782        assert!(json.contains(r#""file":"output.js""#));
1783        assert!(json.contains(r#""sources":["input.js"]"#));
1784
1785        // Verify roundtrip with parser
1786        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1787        let loc = sm.original_position_for(0, 0).unwrap();
1788        assert_eq!(sm.source(loc.source), "input.js");
1789        assert_eq!(loc.line, 0);
1790        assert_eq!(loc.column, 0);
1791    }
1792
1793    #[test]
1794    fn mapping_with_name() {
1795        let mut builder = SourceMapGenerator::new(None);
1796        let src = builder.add_source("input.js");
1797        let name = builder.add_name("myFunction");
1798        builder.add_named_mapping(0, 0, src, 0, 0, name);
1799
1800        let json = builder.to_json();
1801        assert!(json.contains(r#""names":["myFunction"]"#));
1802
1803        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1804        let loc = sm.original_position_for(0, 0).unwrap();
1805        assert_eq!(loc.name, Some(0));
1806        assert_eq!(sm.name(0), "myFunction");
1807    }
1808
1809    #[test]
1810    fn multiple_lines() {
1811        let mut builder = SourceMapGenerator::new(None);
1812        let src = builder.add_source("input.js");
1813        builder.add_mapping(0, 0, src, 0, 0);
1814        builder.add_mapping(1, 4, src, 1, 2);
1815        builder.add_mapping(2, 0, src, 2, 0);
1816
1817        let json = builder.to_json();
1818        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1819        assert_eq!(sm.line_count(), 3);
1820
1821        let loc = sm.original_position_for(1, 4).unwrap();
1822        assert_eq!(loc.line, 1);
1823        assert_eq!(loc.column, 2);
1824    }
1825
1826    #[test]
1827    fn multiple_sources() {
1828        let mut builder = SourceMapGenerator::new(None);
1829        let a = builder.add_source("a.js");
1830        let b = builder.add_source("b.js");
1831        builder.add_mapping(0, 0, a, 0, 0);
1832        builder.add_mapping(1, 0, b, 0, 0);
1833
1834        let json = builder.to_json();
1835        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1836
1837        let loc0 = sm.original_position_for(0, 0).unwrap();
1838        let loc1 = sm.original_position_for(1, 0).unwrap();
1839        assert_eq!(sm.source(loc0.source), "a.js");
1840        assert_eq!(sm.source(loc1.source), "b.js");
1841    }
1842
1843    #[test]
1844    fn source_content() {
1845        let mut builder = SourceMapGenerator::new(None);
1846        let src = builder.add_source("input.js");
1847        builder.set_source_content(src, "var x = 1;".to_string());
1848        builder.add_mapping(0, 0, src, 0, 0);
1849
1850        let json = builder.to_json();
1851        assert!(json.contains(r#""sourcesContent":["var x = 1;"]"#));
1852
1853        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1854        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
1855    }
1856
1857    #[test]
1858    fn source_root() {
1859        let mut builder = SourceMapGenerator::new(None);
1860        builder.set_source_root("src/".to_string());
1861        let src = builder.add_source("input.js");
1862        builder.add_mapping(0, 0, src, 0, 0);
1863
1864        let json = builder.to_json();
1865        assert!(json.contains(r#""sourceRoot":"src/""#));
1866    }
1867
1868    #[test]
1869    fn ignore_list() {
1870        let mut builder = SourceMapGenerator::new(None);
1871        let _app = builder.add_source("app.js");
1872        let lib = builder.add_source("node_modules/lib.js");
1873        builder.add_to_ignore_list(lib);
1874        builder.add_mapping(0, 0, lib, 0, 0);
1875
1876        let json = builder.to_json();
1877        assert!(json.contains(r#""ignoreList":[1]"#));
1878    }
1879
1880    #[test]
1881    fn generated_only_mapping() {
1882        let mut builder = SourceMapGenerator::new(None);
1883        builder.add_generated_mapping(0, 0);
1884
1885        let json = builder.to_json();
1886        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1887        // Generated-only mapping → no source info
1888        assert!(sm.original_position_for(0, 0).is_none());
1889    }
1890
1891    #[test]
1892    fn dedup_sources_and_names() {
1893        let mut builder = SourceMapGenerator::new(None);
1894        let s1 = builder.add_source("input.js");
1895        let s2 = builder.add_source("input.js"); // duplicate
1896        assert_eq!(s1, s2);
1897
1898        let n1 = builder.add_name("foo");
1899        let n2 = builder.add_name("foo"); // duplicate
1900        assert_eq!(n1, n2);
1901
1902        assert_eq!(builder.sources.len(), 1);
1903        assert_eq!(builder.names.len(), 1);
1904    }
1905
1906    #[test]
1907    fn large_roundtrip() {
1908        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
1909
1910        for i in 0..5 {
1911            builder.add_source(&format!("src/file{i}.js"));
1912        }
1913        for i in 0..10 {
1914            builder.add_name(&format!("var{i}"));
1915        }
1916
1917        // Add 1000 mappings across 100 lines
1918        for line in 0..100u32 {
1919            for col in 0..10u32 {
1920                let src = (line * 10 + col) % 5;
1921                let name = if col % 3 == 0 { Some(col % 10) } else { None };
1922
1923                match name {
1924                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
1925                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
1926                }
1927            }
1928        }
1929
1930        let json = builder.to_json();
1931        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1932
1933        assert_eq!(sm.mapping_count(), 1000);
1934        assert_eq!(sm.line_count(), 100);
1935
1936        // Verify a few lookups
1937        let loc = sm.original_position_for(50, 30).unwrap();
1938        assert_eq!(loc.line, 50);
1939        assert_eq!(loc.column, 15);
1940    }
1941
1942    #[test]
1943    fn json_escaping() {
1944        let mut builder = SourceMapGenerator::new(None);
1945        let src = builder.add_source("path/with\"quotes.js");
1946        builder.set_source_content(src, "line1\nline2\ttab".to_string());
1947        builder.add_mapping(0, 0, src, 0, 0);
1948
1949        let json = builder.to_json();
1950        // Should be valid JSON
1951        let _: serde_json::Value = serde_json::from_str(&json).unwrap();
1952    }
1953
1954    #[test]
1955    fn maybe_add_mapping_skips_redundant() {
1956        let mut builder = SourceMapGenerator::new(None);
1957        let src = builder.add_source("input.js");
1958
1959        // First mapping — always added
1960        assert!(builder.maybe_add_mapping(0, 0, src, 10, 0));
1961        // Same source position, different generated column — redundant, skipped
1962        assert!(!builder.maybe_add_mapping(0, 5, src, 10, 0));
1963        // Different source position — added
1964        assert!(builder.maybe_add_mapping(0, 10, src, 11, 0));
1965        // Different generated line, same source position as last — added (new line resets)
1966        assert!(builder.maybe_add_mapping(1, 0, src, 11, 0));
1967
1968        assert_eq!(builder.mapping_count(), 3);
1969
1970        let json = builder.to_json();
1971        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
1972        assert_eq!(sm.mapping_count(), 3);
1973    }
1974
1975    #[test]
1976    fn maybe_add_mapping_different_source() {
1977        let mut builder = SourceMapGenerator::new(None);
1978        let a = builder.add_source("a.js");
1979        let b = builder.add_source("b.js");
1980
1981        assert!(builder.maybe_add_mapping(0, 0, a, 0, 0));
1982        // Same line/col but different source — not redundant
1983        assert!(builder.maybe_add_mapping(0, 5, b, 0, 0));
1984
1985        assert_eq!(builder.mapping_count(), 2);
1986    }
1987
1988    #[test]
1989    fn to_decoded_map_basic() {
1990        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
1991        let src = builder.add_source("input.js");
1992        builder.add_mapping(0, 0, src, 0, 0);
1993        builder.add_mapping(1, 4, src, 1, 2);
1994
1995        let sm = builder.to_decoded_map();
1996        assert_eq!(sm.mapping_count(), 2);
1997        assert_eq!(sm.line_count(), 2);
1998
1999        let loc = sm.original_position_for(0, 0).unwrap();
2000        assert_eq!(sm.source(loc.source), "input.js");
2001        assert_eq!(loc.line, 0);
2002        assert_eq!(loc.column, 0);
2003
2004        let loc = sm.original_position_for(1, 4).unwrap();
2005        assert_eq!(loc.line, 1);
2006        assert_eq!(loc.column, 2);
2007    }
2008
2009    #[test]
2010    fn to_decoded_map_with_names() {
2011        let mut builder = SourceMapGenerator::new(None);
2012        let src = builder.add_source("input.js");
2013        let name = builder.add_name("myFunction");
2014        builder.add_named_mapping(0, 0, src, 0, 0, name);
2015
2016        let sm = builder.to_decoded_map();
2017        let loc = sm.original_position_for(0, 0).unwrap();
2018        assert_eq!(loc.name, Some(0));
2019        assert_eq!(sm.name(0), "myFunction");
2020    }
2021
2022    #[test]
2023    fn to_decoded_map_matches_json_roundtrip() {
2024        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
2025        for i in 0..5 {
2026            builder.add_source(&format!("src/file{i}.js"));
2027        }
2028        for i in 0..10 {
2029            builder.add_name(&format!("var{i}"));
2030        }
2031
2032        for line in 0..50u32 {
2033            for col in 0..10u32 {
2034                let src = (line * 10 + col) % 5;
2035                let name = if col % 3 == 0 { Some(col % 10) } else { None };
2036                match name {
2037                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
2038                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
2039                }
2040            }
2041        }
2042
2043        // Compare decoded map vs JSON roundtrip
2044        let sm_decoded = builder.to_decoded_map();
2045        let json = builder.to_json();
2046        let sm_json = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2047
2048        assert_eq!(sm_decoded.mapping_count(), sm_json.mapping_count());
2049        assert_eq!(sm_decoded.line_count(), sm_json.line_count());
2050
2051        // Verify all lookups match
2052        for m in sm_json.all_mappings() {
2053            let a = sm_json.original_position_for(m.generated_line, m.generated_column);
2054            let b = sm_decoded.original_position_for(m.generated_line, m.generated_column);
2055            match (a, b) {
2056                (Some(a), Some(b)) => {
2057                    assert_eq!(
2058                        a.source, b.source,
2059                        "source mismatch at ({}, {})",
2060                        m.generated_line, m.generated_column
2061                    );
2062                    assert_eq!(
2063                        a.line, b.line,
2064                        "line mismatch at ({}, {})",
2065                        m.generated_line, m.generated_column
2066                    );
2067                    assert_eq!(
2068                        a.column, b.column,
2069                        "column mismatch at ({}, {})",
2070                        m.generated_line, m.generated_column
2071                    );
2072                    assert_eq!(
2073                        a.name, b.name,
2074                        "name mismatch at ({}, {})",
2075                        m.generated_line, m.generated_column
2076                    );
2077                }
2078                (None, None) => {}
2079                _ => panic!("lookup mismatch at ({}, {})", m.generated_line, m.generated_column),
2080            }
2081        }
2082    }
2083
2084    #[test]
2085    fn to_decoded_map_empty() {
2086        let builder = SourceMapGenerator::new(None);
2087        let sm = builder.to_decoded_map();
2088        assert_eq!(sm.mapping_count(), 0);
2089        assert_eq!(sm.line_count(), 0);
2090    }
2091
2092    #[test]
2093    fn to_decoded_map_generated_only() {
2094        let mut builder = SourceMapGenerator::new(None);
2095        builder.add_generated_mapping(0, 0);
2096
2097        let sm = builder.to_decoded_map();
2098        assert_eq!(sm.mapping_count(), 1);
2099        // Generated-only mapping has no source info
2100        assert!(sm.original_position_for(0, 0).is_none());
2101    }
2102
2103    #[test]
2104    fn to_decoded_map_multiple_sources() {
2105        let mut builder = SourceMapGenerator::new(None);
2106        let a = builder.add_source("a.js");
2107        let b = builder.add_source("b.js");
2108        builder.add_mapping(0, 0, a, 0, 0);
2109        builder.add_mapping(1, 0, b, 0, 0);
2110
2111        let sm = builder.to_decoded_map();
2112        let loc0 = sm.original_position_for(0, 0).unwrap();
2113        let loc1 = sm.original_position_for(1, 0).unwrap();
2114        assert_eq!(sm.source(loc0.source), "a.js");
2115        assert_eq!(sm.source(loc1.source), "b.js");
2116    }
2117
2118    #[test]
2119    fn to_decoded_map_with_source_content() {
2120        let mut builder = SourceMapGenerator::new(None);
2121        let src = builder.add_source("input.js");
2122        builder.set_source_content(src, "var x = 1;".to_string());
2123        builder.add_mapping(0, 0, src, 0, 0);
2124
2125        let sm = builder.to_decoded_map();
2126        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
2127    }
2128
2129    #[test]
2130    fn to_decoded_map_reverse_lookup() {
2131        let mut builder = SourceMapGenerator::new(None);
2132        let src = builder.add_source("input.js");
2133        builder.add_mapping(0, 0, src, 10, 5);
2134
2135        let sm = builder.to_decoded_map();
2136        let loc = sm.generated_position_for("input.js", 10, 5).unwrap();
2137        assert_eq!(loc.line, 0);
2138        assert_eq!(loc.column, 0);
2139    }
2140
2141    #[test]
2142    fn out_of_order_insertion_is_sorted_on_encode() {
2143        // Insert mappings in reverse position order. The incremental
2144        // `mappings_in_order` flag must flip to false so encode sorts them,
2145        // producing identical output to in-order insertion.
2146        let mut unsorted = SourceMapGenerator::new(None);
2147        let src = unsorted.add_source("input.js");
2148        unsorted.add_mapping(2, 0, src, 2, 0);
2149        unsorted.add_mapping(0, 5, src, 0, 5);
2150        unsorted.add_mapping(0, 0, src, 0, 0);
2151        unsorted.add_mapping(1, 0, src, 1, 0);
2152
2153        let mut sorted = SourceMapGenerator::new(None);
2154        let src = sorted.add_source("input.js");
2155        sorted.add_mapping(0, 0, src, 0, 0);
2156        sorted.add_mapping(0, 5, src, 0, 5);
2157        sorted.add_mapping(1, 0, src, 1, 0);
2158        sorted.add_mapping(2, 0, src, 2, 0);
2159
2160        assert_eq!(unsorted.encode_mappings(), sorted.encode_mappings());
2161
2162        // And the decoded map resolves the reordered positions correctly.
2163        let sm = unsorted.to_decoded_map();
2164        let loc = sm.original_position_for(0, 5).unwrap();
2165        assert_eq!((loc.line, loc.column), (0, 5));
2166        let loc = sm.original_position_for(2, 0).unwrap();
2167        assert_eq!((loc.line, loc.column), (2, 0));
2168    }
2169
2170    #[test]
2171    fn to_decoded_map_sparse_lines() {
2172        let mut builder = SourceMapGenerator::new(None);
2173        let src = builder.add_source("input.js");
2174        builder.add_mapping(0, 0, src, 0, 0);
2175        builder.add_mapping(5, 0, src, 5, 0);
2176
2177        let sm = builder.to_decoded_map();
2178        assert_eq!(sm.line_count(), 6);
2179        assert!(sm.original_position_for(0, 0).is_some());
2180        assert!(sm.original_position_for(2, 0).is_none());
2181        assert!(sm.original_position_for(5, 0).is_some());
2182    }
2183
2184    #[test]
2185    fn empty_lines_between_mappings() {
2186        let mut builder = SourceMapGenerator::new(None);
2187        let src = builder.add_source("input.js");
2188        builder.add_mapping(0, 0, src, 0, 0);
2189        // Skip lines 1-4
2190        builder.add_mapping(5, 0, src, 5, 0);
2191
2192        let json = builder.to_json();
2193        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2194
2195        // Line 0 should have a mapping
2196        assert!(sm.original_position_for(0, 0).is_some());
2197        // Lines 1-4 should have no mappings
2198        assert!(sm.original_position_for(2, 0).is_none());
2199        // Line 5 should have a mapping
2200        assert!(sm.original_position_for(5, 0).is_some());
2201    }
2202
2203    #[test]
2204    fn debug_id() {
2205        let mut builder = SourceMapGenerator::new(None);
2206        builder.set_debug_id("85314830-023f-4cf1-a267-535f4e37bb17".to_string());
2207        let src = builder.add_source("input.js");
2208        builder.add_mapping(0, 0, src, 0, 0);
2209
2210        let json = builder.to_json();
2211        assert!(json.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
2212
2213        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2214        assert_eq!(sm.debug_id.as_deref(), Some("85314830-023f-4cf1-a267-535f4e37bb17"));
2215    }
2216
2217    #[test]
2218    fn scopes_roundtrip() {
2219        use srcmap_scopes::{Binding, GeneratedRange, OriginalScope, Position, ScopeInfo};
2220
2221        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
2222        let src = builder.add_source("input.js");
2223        builder.set_source_content(
2224            src,
2225            "function hello(name) {\n  return name;\n}\nhello('world');".to_string(),
2226        );
2227        let name_hello = builder.add_name("hello");
2228        builder.add_named_mapping(0, 0, src, 0, 0, name_hello);
2229        builder.add_mapping(1, 0, src, 1, 0);
2230
2231        // Set scopes
2232        builder.set_scopes(ScopeInfo {
2233            scopes: vec![Some(OriginalScope {
2234                start: Position { line: 0, column: 0 },
2235                end: Position { line: 3, column: 14 },
2236                name: None,
2237                kind: Some("global".to_string()),
2238                is_stack_frame: false,
2239                variables: vec!["hello".to_string()],
2240                children: vec![OriginalScope {
2241                    start: Position { line: 0, column: 9 },
2242                    end: Position { line: 2, column: 1 },
2243                    name: Some("hello".to_string()),
2244                    kind: Some("function".to_string()),
2245                    is_stack_frame: true,
2246                    variables: vec!["name".to_string()],
2247                    children: vec![],
2248                }],
2249            })],
2250            ranges: vec![GeneratedRange {
2251                start: Position { line: 0, column: 0 },
2252                end: Position { line: 3, column: 14 },
2253                is_stack_frame: false,
2254                is_hidden: false,
2255                definition: Some(0),
2256                call_site: None,
2257                bindings: vec![Binding::Expression("hello".to_string())],
2258                children: vec![GeneratedRange {
2259                    start: Position { line: 0, column: 9 },
2260                    end: Position { line: 2, column: 1 },
2261                    is_stack_frame: true,
2262                    is_hidden: false,
2263                    definition: Some(1),
2264                    call_site: None,
2265                    bindings: vec![Binding::Expression("name".to_string())],
2266                    children: vec![],
2267                }],
2268            }],
2269        });
2270
2271        let json = builder.to_json();
2272
2273        // Verify scopes field is present
2274        assert!(json.contains(r#""scopes":"#));
2275
2276        // Parse back and verify
2277        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2278        assert!(sm.scopes.is_some());
2279
2280        let scopes_info = sm.scopes.unwrap();
2281
2282        // Verify original scopes
2283        assert_eq!(scopes_info.scopes.len(), 1);
2284        let root_scope = scopes_info.scopes[0].as_ref().unwrap();
2285        assert_eq!(root_scope.kind.as_deref(), Some("global"));
2286        assert_eq!(root_scope.variables, vec!["hello"]);
2287        assert_eq!(root_scope.children.len(), 1);
2288
2289        let fn_scope = &root_scope.children[0];
2290        assert_eq!(fn_scope.name.as_deref(), Some("hello"));
2291        assert_eq!(fn_scope.kind.as_deref(), Some("function"));
2292        assert!(fn_scope.is_stack_frame);
2293        assert_eq!(fn_scope.variables, vec!["name"]);
2294
2295        // Verify generated ranges
2296        assert_eq!(scopes_info.ranges.len(), 1);
2297        let outer = &scopes_info.ranges[0];
2298        assert_eq!(outer.definition, Some(0));
2299        assert_eq!(outer.bindings, vec![Binding::Expression("hello".to_string())]);
2300        assert_eq!(outer.children.len(), 1);
2301
2302        let inner = &outer.children[0];
2303        assert_eq!(inner.definition, Some(1));
2304        assert!(inner.is_stack_frame);
2305        assert_eq!(inner.bindings, vec![Binding::Expression("name".to_string())]);
2306    }
2307
2308    #[test]
2309    fn scopes_with_inlining_roundtrip() {
2310        use srcmap_scopes::{
2311            Binding, CallSite, GeneratedRange, OriginalScope, Position, ScopeInfo,
2312        };
2313
2314        let mut builder = SourceMapGenerator::new(None);
2315        let src = builder.add_source("input.js");
2316        builder.add_mapping(0, 0, src, 0, 0);
2317
2318        builder.set_scopes(ScopeInfo {
2319            scopes: vec![Some(OriginalScope {
2320                start: Position { line: 0, column: 0 },
2321                end: Position { line: 10, column: 0 },
2322                name: None,
2323                kind: None,
2324                is_stack_frame: false,
2325                variables: vec!["x".to_string()],
2326                children: vec![OriginalScope {
2327                    start: Position { line: 1, column: 0 },
2328                    end: Position { line: 4, column: 1 },
2329                    name: Some("greet".to_string()),
2330                    kind: Some("function".to_string()),
2331                    is_stack_frame: true,
2332                    variables: vec!["msg".to_string()],
2333                    children: vec![],
2334                }],
2335            })],
2336            ranges: vec![GeneratedRange {
2337                start: Position { line: 0, column: 0 },
2338                end: Position { line: 10, column: 0 },
2339                is_stack_frame: false,
2340                is_hidden: false,
2341                definition: Some(0),
2342                call_site: None,
2343                bindings: vec![Binding::Expression("_x".to_string())],
2344                children: vec![GeneratedRange {
2345                    start: Position { line: 6, column: 0 },
2346                    end: Position { line: 8, column: 0 },
2347                    is_stack_frame: true,
2348                    is_hidden: false,
2349                    definition: Some(1),
2350                    call_site: Some(CallSite { source_index: 0, line: 8, column: 0 }),
2351                    bindings: vec![Binding::Expression("\"Hello\"".to_string())],
2352                    children: vec![],
2353                }],
2354            }],
2355        });
2356
2357        let json = builder.to_json();
2358        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2359        let info = sm.scopes.unwrap();
2360
2361        // Verify call site on inlined range
2362        let inlined = &info.ranges[0].children[0];
2363        assert_eq!(inlined.call_site, Some(CallSite { source_index: 0, line: 8, column: 0 }));
2364        assert_eq!(inlined.bindings, vec![Binding::Expression("\"Hello\"".to_string())]);
2365    }
2366
2367    #[test]
2368    fn set_source_content_out_of_bounds() {
2369        let mut builder = SourceMapGenerator::new(None);
2370        // No sources added, index 0 is out of bounds
2371        builder.set_source_content(0, "content".to_string());
2372        // Should silently do nothing
2373        let json = builder.to_json();
2374        assert!(!json.contains("content"));
2375    }
2376
2377    #[test]
2378    fn add_to_ignore_list_dedup() {
2379        let mut builder = SourceMapGenerator::new(None);
2380        let idx = builder.add_source("vendor.js");
2381        builder.add_to_ignore_list(idx);
2382        builder.add_to_ignore_list(idx); // duplicate - should be deduped
2383        let json = builder.to_json();
2384        // Should only appear once in ignoreList
2385        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2386        assert_eq!(sm.ignore_list, vec![0]);
2387    }
2388
2389    #[test]
2390    fn to_decoded_map_with_source_root() {
2391        let mut builder = SourceMapGenerator::new(None);
2392        builder.set_source_root("src/".to_string());
2393        let src = builder.add_source("app.ts");
2394        builder.add_mapping(0, 0, src, 0, 0);
2395        let sm = builder.to_decoded_map();
2396        // Sources should be prefixed with source_root
2397        assert_eq!(sm.sources, vec!["src/app.ts"]);
2398    }
2399
2400    #[test]
2401    fn json_escaping_special_chars() {
2402        let mut builder = SourceMapGenerator::new(None);
2403        let src = builder.add_source("a.js");
2404        // Content with special chars: quotes, backslash, newline, carriage return, tab, control char
2405        builder.set_source_content(src, "line1\nline2\r\ttab\\\"\x01end".to_string());
2406        builder.add_mapping(0, 0, src, 0, 0);
2407        let json = builder.to_json();
2408        // Verify it's valid JSON by parsing
2409        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2410        assert_eq!(sm.sources_content, vec![Some("line1\nline2\r\ttab\\\"\x01end".to_string())]);
2411    }
2412
2413    #[test]
2414    fn json_escaping_in_names() {
2415        let mut builder = SourceMapGenerator::new(None);
2416        let src = builder.add_source("a.js");
2417        let name = builder.add_name("func\"with\\special");
2418        builder.add_named_mapping(0, 0, src, 0, 0, name);
2419        let json = builder.to_json();
2420        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2421        assert_eq!(sm.names[0], "func\"with\\special");
2422    }
2423
2424    #[test]
2425    fn json_escaping_in_sources() {
2426        let mut builder = SourceMapGenerator::new(None);
2427        let src = builder.add_source("path/with\"quotes.js");
2428        builder.add_mapping(0, 0, src, 0, 0);
2429        let json = builder.to_json();
2430        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2431        assert_eq!(sm.sources[0], "path/with\"quotes.js");
2432    }
2433
2434    #[cfg(feature = "parallel")]
2435    mod parallel_tests {
2436        use super::*;
2437
2438        fn build_large_generator(lines: u32, cols_per_line: u32) -> SourceMapGenerator {
2439            let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
2440            for i in 0..10 {
2441                let src = builder.add_source(&format!("src/file{i}.js"));
2442                builder.set_source_content(
2443                    src,
2444                    format!("// source file {i}\n{}", "x = 1;\n".repeat(100)),
2445                );
2446            }
2447            for i in 0..20 {
2448                builder.add_name(&format!("var{i}"));
2449            }
2450
2451            for line in 0..lines {
2452                for col in 0..cols_per_line {
2453                    let src = (line * cols_per_line + col) % 10;
2454                    let name = if col % 3 == 0 { Some(col % 20) } else { None };
2455                    match name {
2456                        Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
2457                        None => builder.add_mapping(line, col * 10, src, line, col * 5),
2458                    }
2459                }
2460            }
2461            builder
2462        }
2463
2464        #[test]
2465        fn parallel_large_roundtrip() {
2466            let builder = build_large_generator(500, 20);
2467            let json = builder.to_json();
2468            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2469            assert_eq!(sm.mapping_count(), 10000);
2470            assert_eq!(sm.line_count(), 500);
2471
2472            // Verify lookups
2473            let loc = sm.original_position_for(250, 50).unwrap();
2474            assert_eq!(loc.line, 250);
2475            assert_eq!(loc.column, 25);
2476        }
2477
2478        #[test]
2479        fn parallel_matches_sequential() {
2480            let builder = build_large_generator(500, 20);
2481
2482            // Sort mappings the same way encode_mappings does
2483            let mut sorted: Vec<&Mapping> = builder.mappings.iter().collect();
2484            sorted.sort_unstable_by(|a, b| {
2485                a.generated_line
2486                    .cmp(&b.generated_line)
2487                    .then(a.generated_column.cmp(&b.generated_column))
2488            });
2489
2490            let sequential = SourceMapGenerator::encode_sequential_impl(&sorted);
2491            let parallel = SourceMapGenerator::encode_parallel_impl(&sorted);
2492            assert_eq!(sequential, parallel);
2493        }
2494
2495        #[test]
2496        fn parallel_with_sparse_lines() {
2497            let mut builder = SourceMapGenerator::new(None);
2498            let src = builder.add_source("input.js");
2499
2500            // Add mappings on lines 0, 100, 200, ... (sparse)
2501            for i in 0..50 {
2502                let line = i * 100;
2503                for col in 0..100u32 {
2504                    builder.add_mapping(line, col * 10, src, line, col * 5);
2505                }
2506            }
2507
2508            let json = builder.to_json();
2509            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2510            assert_eq!(sm.mapping_count(), 5000);
2511
2512            // Verify empty lines have no mappings
2513            assert!(sm.original_position_for(50, 0).is_none());
2514            // Verify populated lines work
2515            let loc = sm.original_position_for(200, 50).unwrap();
2516            assert_eq!(loc.line, 200);
2517            assert_eq!(loc.column, 25);
2518        }
2519    }
2520
2521    // ── StreamingGenerator tests ────────────────────────────────
2522
2523    #[test]
2524    fn streaming_basic() {
2525        let mut sg = StreamingGenerator::new(Some("out.js".to_string()));
2526        let src = sg.add_source("input.js");
2527        sg.add_mapping(0, 0, src, 0, 0);
2528        sg.add_mapping(1, 0, src, 1, 0);
2529
2530        let json = sg.to_json();
2531        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2532        assert_eq!(sm.sources, vec!["input.js"]);
2533        assert_eq!(sm.mapping_count(), 2);
2534
2535        let loc0 = sm.original_position_for(0, 0).unwrap();
2536        assert_eq!(sm.source(loc0.source), "input.js");
2537        assert_eq!(loc0.line, 0);
2538
2539        let loc1 = sm.original_position_for(1, 0).unwrap();
2540        assert_eq!(loc1.line, 1);
2541    }
2542
2543    #[test]
2544    fn streaming_with_names() {
2545        let mut sg = StreamingGenerator::new(None);
2546        let src = sg.add_source("a.js");
2547        let name = sg.add_name("foo");
2548        sg.add_named_mapping(0, 0, src, 0, 0, name);
2549
2550        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2551        let loc = sm.original_position_for(0, 0).unwrap();
2552        assert_eq!(loc.name, Some(0));
2553        assert_eq!(sm.name(0), "foo");
2554    }
2555
2556    #[test]
2557    fn streaming_generated_only() {
2558        let mut sg = StreamingGenerator::new(None);
2559        let src = sg.add_source("a.js");
2560        sg.add_generated_mapping(0, 0);
2561        sg.add_mapping(0, 5, src, 0, 0);
2562
2563        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2564        assert_eq!(sm.mapping_count(), 2);
2565        assert!(sm.original_position_for(0, 0).is_none());
2566        assert!(sm.original_position_for(0, 5).is_some());
2567    }
2568
2569    #[test]
2570    fn streaming_matches_regular_generator() {
2571        let mut regular = SourceMapGenerator::new(Some("out.js".to_string()));
2572        let mut streaming = StreamingGenerator::new(Some("out.js".to_string()));
2573
2574        let src_r = regular.add_source("a.js");
2575        let src_s = streaming.add_source("a.js");
2576
2577        let name_r = regular.add_name("hello");
2578        let name_s = streaming.add_name("hello");
2579
2580        regular.set_source_content(src_r, "var hello;".to_string());
2581        streaming.set_source_content(src_s, "var hello;".to_string());
2582
2583        regular.add_named_mapping(0, 0, src_r, 0, 0, name_r);
2584        streaming.add_named_mapping(0, 0, src_s, 0, 0, name_s);
2585
2586        regular.add_mapping(0, 10, src_r, 0, 4);
2587        streaming.add_mapping(0, 10, src_s, 0, 4);
2588
2589        regular.add_mapping(1, 0, src_r, 1, 0);
2590        streaming.add_mapping(1, 0, src_s, 1, 0);
2591
2592        let sm_r = srcmap_sourcemap::SourceMap::from_json(&regular.to_json()).unwrap();
2593        let sm_s = srcmap_sourcemap::SourceMap::from_json(&streaming.to_json()).unwrap();
2594
2595        assert_eq!(sm_r.mapping_count(), sm_s.mapping_count());
2596        assert_eq!(sm_r.sources, sm_s.sources);
2597        assert_eq!(sm_r.names, sm_s.names);
2598        assert_eq!(sm_r.sources_content, sm_s.sources_content);
2599
2600        for (a, b) in sm_r.all_mappings().iter().zip(sm_s.all_mappings().iter()) {
2601            assert_eq!(a.generated_line, b.generated_line);
2602            assert_eq!(a.generated_column, b.generated_column);
2603            assert_eq!(a.source, b.source);
2604            assert_eq!(a.original_line, b.original_line);
2605            assert_eq!(a.original_column, b.original_column);
2606            assert_eq!(a.name, b.name);
2607        }
2608    }
2609
2610    #[test]
2611    fn streaming_to_decoded_map() {
2612        let mut sg = StreamingGenerator::new(None);
2613        let src = sg.add_source("test.js");
2614        sg.add_mapping(0, 0, src, 0, 0);
2615        sg.add_mapping(2, 5, src, 1, 3);
2616
2617        let sm = sg.to_decoded_map().unwrap();
2618        assert_eq!(sm.mapping_count(), 2);
2619        assert_eq!(sm.sources, vec!["test.js"]);
2620
2621        let loc = sm.original_position_for(2, 5).unwrap();
2622        assert_eq!(loc.line, 1);
2623        assert_eq!(loc.column, 3);
2624    }
2625
2626    #[test]
2627    fn streaming_source_dedup() {
2628        let mut sg = StreamingGenerator::new(None);
2629        let src1 = sg.add_source("a.js");
2630        let src2 = sg.add_source("a.js");
2631        assert_eq!(src1, src2);
2632        assert_eq!(sg.sources.len(), 1);
2633    }
2634
2635    #[test]
2636    fn streaming_ignore_list() {
2637        let mut sg = StreamingGenerator::new(None);
2638        let src = sg.add_source("vendor.js");
2639        sg.add_to_ignore_list(src);
2640        sg.add_mapping(0, 0, src, 0, 0);
2641
2642        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2643        assert_eq!(sm.ignore_list, vec![0]);
2644    }
2645
2646    #[test]
2647    fn streaming_empty() {
2648        let sg = StreamingGenerator::new(None);
2649        let json = sg.to_json();
2650        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2651        assert_eq!(sm.mapping_count(), 0);
2652    }
2653
2654    #[test]
2655    fn streaming_sparse_lines() {
2656        let mut sg = StreamingGenerator::new(None);
2657        let src = sg.add_source("a.js");
2658        sg.add_mapping(0, 0, src, 0, 0);
2659        sg.add_mapping(5, 0, src, 5, 0);
2660
2661        let sm = srcmap_sourcemap::SourceMap::from_json(&sg.to_json()).unwrap();
2662        assert_eq!(sm.mapping_count(), 2);
2663        assert!(sm.original_position_for(0, 0).is_some());
2664        assert!(sm.original_position_for(5, 0).is_some());
2665    }
2666
2667    // ── Range mapping tests ───────────────────────────────────
2668
2669    #[test]
2670    fn range_mapping_basic() {
2671        let mut builder = SourceMapGenerator::new(None);
2672        let src = builder.add_source("input.js");
2673        builder.add_range_mapping(0, 0, src, 0, 0);
2674        builder.add_mapping(0, 5, src, 0, 10);
2675
2676        let json = builder.to_json();
2677        assert!(json.contains(r#""rangeMappings":"A""#));
2678    }
2679
2680    #[test]
2681    fn range_mapping_multiple_on_line() {
2682        let mut builder = SourceMapGenerator::new(None);
2683        let src = builder.add_source("input.js");
2684        builder.add_range_mapping(0, 0, src, 0, 0);
2685        builder.add_mapping(0, 5, src, 0, 10);
2686        builder.add_range_mapping(0, 10, src, 0, 20);
2687
2688        let json = builder.to_json();
2689        assert!(json.contains(r#""rangeMappings":"A,C""#));
2690    }
2691
2692    #[test]
2693    fn range_mapping_multi_line() {
2694        let mut builder = SourceMapGenerator::new(None);
2695        let src = builder.add_source("input.js");
2696        builder.add_range_mapping(0, 0, src, 0, 0);
2697        builder.add_range_mapping(1, 0, src, 1, 0);
2698
2699        let json = builder.to_json();
2700        assert!(json.contains(r#""rangeMappings":"A;A""#));
2701    }
2702
2703    #[test]
2704    fn no_range_mappings_omits_field() {
2705        let mut builder = SourceMapGenerator::new(None);
2706        let src = builder.add_source("input.js");
2707        builder.add_mapping(0, 0, src, 0, 0);
2708
2709        let json = builder.to_json();
2710        assert!(!json.contains("rangeMappings"));
2711    }
2712
2713    #[test]
2714    fn named_range_mapping() {
2715        let mut builder = SourceMapGenerator::new(None);
2716        let src = builder.add_source("input.js");
2717        let name = builder.add_name("foo");
2718        builder.add_named_range_mapping(0, 0, src, 0, 0, name);
2719
2720        let json = builder.to_json();
2721        assert!(json.contains(r#""rangeMappings":"A""#));
2722    }
2723
2724    #[test]
2725    fn to_decoded_map_preserves_range_mappings() {
2726        let mut builder = SourceMapGenerator::new(None);
2727        let src = builder.add_source("input.js");
2728        builder.add_range_mapping(0, 0, src, 0, 0);
2729        builder.add_mapping(0, 5, src, 0, 10);
2730
2731        let sm = builder.to_decoded_map();
2732        assert!(sm.has_range_mappings());
2733        let mappings = sm.all_mappings();
2734        assert!(mappings[0].is_range_mapping);
2735        assert!(!mappings[1].is_range_mapping);
2736    }
2737
2738    // ── Streaming range mapping tests ────────────────────────────
2739
2740    #[test]
2741    fn streaming_range_mapping_basic() {
2742        let mut sg = StreamingGenerator::new(None);
2743        let src = sg.add_source("input.js");
2744        sg.add_range_mapping(0, 0, src, 0, 0);
2745        sg.add_mapping(0, 5, src, 0, 10);
2746
2747        let json = sg.to_json();
2748        assert!(json.contains(r#""rangeMappings":"A""#));
2749    }
2750
2751    #[test]
2752    fn streaming_range_mapping_roundtrip() {
2753        let mut sg = StreamingGenerator::new(None);
2754        let src = sg.add_source("input.js");
2755        sg.add_range_mapping(0, 0, src, 0, 0);
2756        sg.add_mapping(0, 5, src, 0, 10);
2757
2758        let sm = sg.to_decoded_map().unwrap();
2759        assert!(sm.has_range_mappings());
2760        let mappings = sm.all_mappings();
2761        assert!(mappings[0].is_range_mapping);
2762        assert!(!mappings[1].is_range_mapping);
2763    }
2764
2765    #[test]
2766    fn streaming_range_and_named_range() {
2767        let mut sg = StreamingGenerator::new(None);
2768        let src = sg.add_source("input.js");
2769        let name = sg.add_name("foo");
2770        sg.add_range_mapping(0, 0, src, 0, 0);
2771        sg.add_named_range_mapping(0, 10, src, 0, 5, name);
2772
2773        let json = sg.to_json();
2774        assert!(json.contains(r#""rangeMappings":"A,B""#));
2775
2776        let sm = sg.to_decoded_map().unwrap();
2777        assert!(sm.has_range_mappings());
2778        let mappings = sm.all_mappings();
2779        assert!(mappings[0].is_range_mapping);
2780        assert!(mappings[1].is_range_mapping);
2781    }
2782
2783    #[test]
2784    fn streaming_range_mapping_matches_regular() {
2785        let mut regular = SourceMapGenerator::new(None);
2786        let mut streaming = StreamingGenerator::new(None);
2787
2788        let src_r = regular.add_source("input.js");
2789        let src_s = streaming.add_source("input.js");
2790
2791        regular.add_range_mapping(0, 0, src_r, 0, 0);
2792        streaming.add_range_mapping(0, 0, src_s, 0, 0);
2793
2794        regular.add_mapping(0, 5, src_r, 0, 10);
2795        streaming.add_mapping(0, 5, src_s, 0, 10);
2796
2797        regular.add_range_mapping(0, 10, src_r, 0, 20);
2798        streaming.add_range_mapping(0, 10, src_s, 0, 20);
2799
2800        regular.add_range_mapping(1, 0, src_r, 1, 0);
2801        streaming.add_range_mapping(1, 0, src_s, 1, 0);
2802
2803        let json_r = regular.to_json();
2804        let json_s = streaming.to_json();
2805
2806        let sm_r = srcmap_sourcemap::SourceMap::from_json(&json_r).unwrap();
2807        let sm_s = srcmap_sourcemap::SourceMap::from_json(&json_s).unwrap();
2808
2809        assert_eq!(sm_r.mapping_count(), sm_s.mapping_count());
2810
2811        for (a, b) in sm_r.all_mappings().iter().zip(sm_s.all_mappings().iter()) {
2812            assert_eq!(a.generated_line, b.generated_line);
2813            assert_eq!(a.generated_column, b.generated_column);
2814            assert_eq!(a.source, b.source);
2815            assert_eq!(a.original_line, b.original_line);
2816            assert_eq!(a.original_column, b.original_column);
2817            assert_eq!(a.name, b.name);
2818            assert_eq!(a.is_range_mapping, b.is_range_mapping);
2819        }
2820    }
2821
2822    // ── into_parts tests ────────────────────────────────────
2823
2824    #[test]
2825    fn into_parts_basic() {
2826        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
2827        let src = builder.add_source("input.js");
2828        builder.set_source_content(src, "var x = 1;".to_string());
2829        let name = builder.add_name("x");
2830        builder.add_named_mapping(0, 0, src, 0, 4, name);
2831        builder.add_mapping(1, 0, src, 1, 0);
2832        builder.set_debug_id("test-id");
2833
2834        let json = builder.to_json();
2835        let sm_json = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
2836
2837        // Rebuild with same data for into_parts
2838        let mut builder2 = SourceMapGenerator::new(Some("output.js".to_string()));
2839        let src2 = builder2.add_source("input.js");
2840        builder2.set_source_content(src2, "var x = 1;".to_string());
2841        let name2 = builder2.add_name("x");
2842        builder2.add_named_mapping(0, 0, src2, 0, 4, name2);
2843        builder2.add_mapping(1, 0, src2, 1, 0);
2844        builder2.set_debug_id("test-id");
2845
2846        let parts = builder2.into_parts();
2847        assert_eq!(parts.file, Some("output.js".to_string()));
2848        assert_eq!(parts.sources, vec!["input.js"]);
2849        assert_eq!(parts.names, vec!["x"]);
2850        assert_eq!(parts.sources_content, vec![Some("var x = 1;".to_string())]);
2851        assert_eq!(parts.debug_id, Some("test-id".to_string()));
2852        assert!(!parts.mappings.is_empty());
2853
2854        // Verify the mappings string produces the same source map
2855        let sm_parts = srcmap_sourcemap::SourceMap::from_vlq(
2856            &parts.mappings,
2857            parts.sources,
2858            parts.names,
2859            parts.file,
2860            parts.source_root,
2861            parts.sources_content,
2862            parts.ignore_list,
2863            parts.debug_id,
2864        )
2865        .unwrap();
2866        assert_eq!(sm_parts.mapping_count(), sm_json.mapping_count());
2867    }
2868
2869    #[test]
2870    fn into_parts_empty() {
2871        let builder = SourceMapGenerator::new(None);
2872        let parts = builder.into_parts();
2873        assert_eq!(parts.file, None);
2874        assert!(parts.mappings.is_empty());
2875        assert!(parts.sources.is_empty());
2876        assert!(parts.names.is_empty());
2877    }
2878
2879    #[test]
2880    fn into_parts_with_ignore_list() {
2881        let mut builder = SourceMapGenerator::new(None);
2882        let src = builder.add_source("vendor.js");
2883        builder.add_to_ignore_list(src);
2884        builder.add_mapping(0, 0, src, 0, 0);
2885
2886        let parts = builder.into_parts();
2887        assert_eq!(parts.ignore_list, vec![0]);
2888    }
2889
2890    #[test]
2891    fn into_parts_with_range_mappings() {
2892        let mut builder = SourceMapGenerator::new(None);
2893        let src = builder.add_source("input.js");
2894        builder.add_range_mapping(0, 0, src, 0, 0);
2895        builder.add_mapping(0, 5, src, 0, 10);
2896
2897        let parts = builder.into_parts();
2898        assert!(parts.range_mappings.is_some());
2899    }
2900
2901    #[test]
2902    fn streaming_into_parts() {
2903        let mut sg = StreamingGenerator::new(Some("out.js".to_string()));
2904        let src = sg.add_source("input.js");
2905        sg.set_source_content(src, "var x = 1;".to_string());
2906        let name = sg.add_name("x");
2907        sg.add_named_mapping(0, 0, src, 0, 4, name);
2908        sg.add_mapping(1, 0, src, 1, 0);
2909
2910        let parts = sg.into_parts();
2911        assert_eq!(parts.file, Some("out.js".to_string()));
2912        assert_eq!(parts.sources, vec!["input.js"]);
2913        assert_eq!(parts.names, vec!["x"]);
2914        assert!(!parts.mappings.is_empty());
2915    }
2916
2917    // ── to_writer tests ────────────────────────────────────
2918
2919    #[test]
2920    fn to_writer_matches_to_json() {
2921        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
2922        let src = builder.add_source("input.js");
2923        builder.set_source_content(src, "var x = 1;".to_string());
2924        let name = builder.add_name("x");
2925        builder.add_named_mapping(0, 0, src, 0, 4, name);
2926        builder.add_mapping(1, 0, src, 1, 0);
2927
2928        let json = builder.to_json();
2929        let mut buf = Vec::new();
2930        builder.to_writer(&mut buf).unwrap();
2931        let writer_output = String::from_utf8(buf).unwrap();
2932
2933        assert_eq!(json, writer_output);
2934    }
2935
2936    #[test]
2937    fn to_writer_empty() {
2938        let builder = SourceMapGenerator::new(None);
2939        let mut buf = Vec::new();
2940        builder.to_writer(&mut buf).unwrap();
2941        let output = String::from_utf8(buf).unwrap();
2942        assert!(output.contains(r#""version":3"#));
2943        assert!(output.contains(r#""mappings":"""#));
2944    }
2945
2946    #[test]
2947    fn to_writer_with_all_fields() {
2948        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
2949        builder.set_source_root("src/");
2950        builder.set_debug_id("test-uuid");
2951        let src = builder.add_source("app.ts");
2952        builder.set_source_content(src, "const x = 1;".to_string());
2953        builder.add_to_ignore_list(src);
2954        let name = builder.add_name("x");
2955        builder.add_named_mapping(0, 0, src, 0, 6, name);
2956
2957        let json = builder.to_json();
2958        let mut buf = Vec::new();
2959        builder.to_writer(&mut buf).unwrap();
2960        let writer_output = String::from_utf8(buf).unwrap();
2961
2962        assert_eq!(json, writer_output);
2963
2964        // Verify it parses correctly
2965        let sm = srcmap_sourcemap::SourceMap::from_json(&writer_output).unwrap();
2966        assert_eq!(sm.source(0), "src/app.ts");
2967        assert_eq!(sm.name(0), "x");
2968    }
2969
2970    #[test]
2971    fn streaming_to_writer_matches_to_json() {
2972        let mut sg = StreamingGenerator::new(Some("out.js".to_string()));
2973        let src = sg.add_source("input.js");
2974        sg.set_source_content(src, "var x = 1;".to_string());
2975        let name = sg.add_name("x");
2976        sg.add_named_mapping(0, 0, src, 0, 4, name);
2977        sg.add_mapping(1, 0, src, 1, 0);
2978
2979        let json = sg.to_json();
2980        let mut buf = Vec::new();
2981        sg.to_writer(&mut buf).unwrap();
2982        let writer_output = String::from_utf8(buf).unwrap();
2983
2984        assert_eq!(json, writer_output);
2985    }
2986}