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