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