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