Skip to main content

turbopack_core/source_map/
structured.rs

1//! A source map kept in structured form until it is actually emitted, so that rewriting or
2//! embedding a map shares its ropes instead of copying its bytes. `sourcesContent` in
3//! particular (the full original source text) would otherwise be copied for every chunking
4//! context that rewrites the `sources` URLs and for every chunk source map that embeds the
5//! module's map.
6//!
7//! Fields are stored as verbatim raw JSON snippets ([`Rope`]s) and only decoded when something
8//! actually needs their values: `sources` is decoded lazily by
9//! [`StructuredSourceMap::rewrite_sources`] when a rewrite changes an entry, and `sourcesContent`
10//! of maps built from swc objects is a list of individually pre-escaped [`Rope`]s that are shared
11//! into every map that embeds them.
12//!
13//! [`StructuredSourceMap::to_rope`] emits fields in the same order as `swc_sourcemap`'s
14//! serializer, so maps built via [`StructuredSourceMap::from_swc_map`] serialize byte-for-byte
15//! identically to what `swc_sourcemap::SourceMap::to_writer` would have produced (pinned by the
16//! golden tests below).
17
18use std::sync::LazyLock;
19
20use anyhow::Result;
21use bincode::{Decode, Encode};
22use serde::Deserialize;
23use serde_json::value::RawValue;
24use turbo_tasks::{NonLocalValue, trace::TraceRawVcs};
25use turbo_tasks_fs::rope::{Rope, RopeBuilder};
26use turbo_tasks_hash::{DeterministicHash, DeterministicHasher};
27
28/// A single source map in structured form. See the module documentation.
29#[derive(Clone, Debug, Default, PartialEq, Eq, Encode, Decode, TraceRawVcs, NonLocalValue)]
30pub struct StructuredSourceMap {
31    // Raw snippet fields hold the field's verbatim JSON value (e.g. `3`, `"..."`, `[...]`).
32    // Declaration order mirrors `swc_sourcemap`'s `RawSourceMap`, which is the emission order.
33    version: Option<Rope>,
34    file: Option<Rope>,
35    sources: Option<SourcesField>,
36    source_root: Option<Rope>,
37    sources_content: Option<SourcesContentField>,
38    sections: Option<Rope>,
39    names: Option<Rope>,
40    scopes: Option<Rope>,
41    range_mappings: Option<Rope>,
42    mappings: Option<Rope>,
43    ignore_list: Option<Rope>,
44    x_facebook_offsets: Option<Rope>,
45    x_metro_module_paths: Option<Rope>,
46    x_facebook_sources: Option<Rope>,
47    debug_id_old: Option<Rope>,
48    debug_id: Option<Rope>,
49}
50
51/// The `sources` field. Kept verbatim until a rewrite actually changes an entry, so maps whose
52/// `sources` cannot be decoded (or are never rewritten) round-trip byte-for-byte.
53#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TraceRawVcs, NonLocalValue)]
54enum SourcesField {
55    /// The field's verbatim JSON value.
56    Raw(Rope),
57    /// Decoded form, produced only by [`StructuredSourceMap::rewrite_sources`] when an entry
58    /// changed.
59    Rewritten(Vec<Option<String>>),
60}
61
62/// The `sourcesContent` field.
63#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TraceRawVcs, NonLocalValue)]
64enum SourcesContentField {
65    /// The field's verbatim JSON value (maps parsed from external bytes). Never decoded — this
66    /// tolerates content JSON that does not decode into Rust strings, e.g. lone surrogate
67    /// escapes.
68    Raw(Rope),
69    /// One entry per source: the pre-escaped JSON value of the source's content — a string
70    /// literal (including quotes) or the literal `null`. Shared into every emitted map (maps
71    /// built via [`StructuredSourceMap::from_swc_map`]).
72    Escaped(Vec<Rope>),
73}
74
75/// Deserialization mirror of [`StructuredSourceMap`]. Unknown fields are dropped.
76#[derive(Deserialize)]
77struct RawFields {
78    version: Option<Box<RawValue>>,
79    file: Option<Box<RawValue>>,
80    sources: Option<Box<RawValue>>,
81    #[serde(rename = "sourceRoot")]
82    source_root: Option<Box<RawValue>>,
83    #[serde(rename = "sourcesContent")]
84    sources_content: Option<Box<RawValue>>,
85    sections: Option<Box<RawValue>>,
86    names: Option<Box<RawValue>>,
87    scopes: Option<Box<RawValue>>,
88    #[serde(rename = "rangeMappings")]
89    range_mappings: Option<Box<RawValue>>,
90    mappings: Option<Box<RawValue>>,
91    #[serde(rename = "ignoreList")]
92    ignore_list: Option<Box<RawValue>>,
93    x_facebook_offsets: Option<Box<RawValue>>,
94    x_metro_module_paths: Option<Box<RawValue>>,
95    x_facebook_sources: Option<Box<RawValue>>,
96    #[serde(rename = "debug_id")]
97    debug_id_old: Option<Box<RawValue>>,
98    #[serde(rename = "debugId")]
99    debug_id: Option<Box<RawValue>>,
100}
101
102fn into_rope(value: Option<Box<RawValue>>) -> Option<Rope> {
103    value.map(|v| Rope::from(v.get().as_bytes().to_vec()))
104}
105
106/// JSON-escapes a source's content into the string-literal form stored in `sources_content`.
107fn escape_content(content: Option<&str>) -> Rope {
108    match content {
109        Some(text) => {
110            Rope::from(serde_json::to_vec(text).expect("string serialization is infallible"))
111        }
112        None => {
113            static NULL: LazyLock<Rope> = LazyLock::new(|| Rope::from("null"));
114            NULL.clone()
115        }
116    }
117}
118
119/// A serde [`Serializer`](serde::Serializer) that splits a struct into its top-level fields,
120/// each serialized to its own JSON value. This lets [`StructuredSourceMap::from_serialize`]
121/// capture a source map's fields directly from its `Serialize` implementation without
122/// materializing — and then re-parsing — the whole serialized document.
123struct FieldSplit;
124
125#[derive(Debug)]
126struct FieldSplitError(String);
127
128impl std::fmt::Display for FieldSplitError {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        self.0.fmt(f)
131    }
132}
133
134impl std::error::Error for FieldSplitError {}
135
136impl serde::ser::Error for FieldSplitError {
137    fn custom<T: std::fmt::Display>(msg: T) -> Self {
138        FieldSplitError(msg.to_string())
139    }
140}
141
142struct FieldSplitStruct {
143    fields: Vec<(&'static str, Vec<u8>)>,
144}
145
146impl serde::ser::SerializeStruct for FieldSplitStruct {
147    type Ok = Vec<(&'static str, Vec<u8>)>;
148    type Error = FieldSplitError;
149
150    fn serialize_field<T: ?Sized + serde::Serialize>(
151        &mut self,
152        key: &'static str,
153        value: &T,
154    ) -> Result<(), FieldSplitError> {
155        let bytes = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
156        self.fields.push((key, bytes));
157        Ok(())
158    }
159
160    fn end(self) -> Result<Self::Ok, FieldSplitError> {
161        Ok(self.fields)
162    }
163}
164
165macro_rules! not_a_struct {
166    ($($f:ident($($arg:ty),*) -> $ret:ty;)*) => {
167        $(fn $f(self, $(_: $arg),*) -> Result<$ret, FieldSplitError> {
168            Err(serde::ser::Error::custom("expected a struct"))
169        })*
170    };
171}
172
173impl serde::Serializer for FieldSplit {
174    type Ok = Vec<(&'static str, Vec<u8>)>;
175    type Error = FieldSplitError;
176    type SerializeSeq = serde::ser::Impossible<Self::Ok, Self::Error>;
177    type SerializeTuple = serde::ser::Impossible<Self::Ok, Self::Error>;
178    type SerializeTupleStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
179    type SerializeTupleVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
180    type SerializeMap = serde::ser::Impossible<Self::Ok, Self::Error>;
181    type SerializeStruct = FieldSplitStruct;
182    type SerializeStructVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
183
184    fn serialize_struct(
185        self,
186        _name: &'static str,
187        len: usize,
188    ) -> Result<FieldSplitStruct, FieldSplitError> {
189        Ok(FieldSplitStruct {
190            fields: Vec::with_capacity(len),
191        })
192    }
193
194    not_a_struct! {
195        serialize_bool(bool) -> Self::Ok;
196        serialize_i8(i8) -> Self::Ok;
197        serialize_i16(i16) -> Self::Ok;
198        serialize_i32(i32) -> Self::Ok;
199        serialize_i64(i64) -> Self::Ok;
200        serialize_u8(u8) -> Self::Ok;
201        serialize_u16(u16) -> Self::Ok;
202        serialize_u32(u32) -> Self::Ok;
203        serialize_u64(u64) -> Self::Ok;
204        serialize_f32(f32) -> Self::Ok;
205        serialize_f64(f64) -> Self::Ok;
206        serialize_char(char) -> Self::Ok;
207        serialize_str(&str) -> Self::Ok;
208        serialize_bytes(&[u8]) -> Self::Ok;
209        serialize_none() -> Self::Ok;
210        serialize_unit() -> Self::Ok;
211        serialize_unit_struct(&'static str) -> Self::Ok;
212        serialize_unit_variant(&'static str, u32, &'static str) -> Self::Ok;
213        serialize_seq(Option<usize>) -> Self::SerializeSeq;
214        serialize_tuple(usize) -> Self::SerializeTuple;
215        serialize_tuple_struct(&'static str, usize) -> Self::SerializeTupleStruct;
216        serialize_tuple_variant(&'static str, u32, &'static str, usize) -> Self::SerializeTupleVariant;
217        serialize_map(Option<usize>) -> Self::SerializeMap;
218        serialize_struct_variant(&'static str, u32, &'static str, usize) -> Self::SerializeStructVariant;
219    }
220
221    fn serialize_some<T: ?Sized + serde::Serialize>(
222        self,
223        _value: &T,
224    ) -> Result<Self::Ok, FieldSplitError> {
225        Err(serde::ser::Error::custom("expected a struct"))
226    }
227
228    fn serialize_newtype_struct<T: ?Sized + serde::Serialize>(
229        self,
230        _name: &'static str,
231        value: &T,
232    ) -> Result<Self::Ok, FieldSplitError> {
233        value.serialize(self)
234    }
235
236    fn serialize_newtype_variant<T: ?Sized + serde::Serialize>(
237        self,
238        _name: &'static str,
239        _variant_index: u32,
240        _variant: &'static str,
241        _value: &T,
242    ) -> Result<Self::Ok, FieldSplitError> {
243        Err(serde::ser::Error::custom("expected a struct"))
244    }
245}
246
247impl StructuredSourceMap {
248    /// Builds from a [`swc_sourcemap::SourceMap`], taking the `sourcesContent` entries out of
249    /// the map before serializing the (now small) remainder. This is the cheap constructor for
250    /// all internally generated maps: the source text is escaped exactly once and never
251    /// round-trips through JSON parsing.
252    pub fn from_swc_map(mut map: swc_sourcemap::SourceMap) -> Result<Self> {
253        let contents: Vec<Rope> = map
254            .source_contents()
255            .map(|content| escape_content(content.map(|c| c.as_str())))
256            .collect();
257        let has_contents = contents.iter().any(|c| c.to_bytes().as_ref() != b"null");
258        for idx in 0..map.get_source_count() {
259            map.set_source_contents(idx, None);
260        }
261        let mut skeleton = Vec::new();
262        map.to_writer(&mut skeleton)?;
263        let mut result = Self::from_json_slice(&skeleton)?;
264        result.sources_content = has_contents.then_some(SourcesContentField::Escaped(contents));
265        Ok(result)
266    }
267
268    /// Parses an arbitrary serialized source map, e.g. one shipped alongside external code.
269    pub fn from_json(map: &Rope) -> Result<Self> {
270        Self::from_json_slice(&map.to_bytes())
271    }
272
273    /// Parses a serialized source map from a byte slice.
274    pub fn from_json_slice(map: &[u8]) -> Result<Self> {
275        let fields: RawFields = serde_json::from_slice(map)?;
276        Ok(StructuredSourceMap {
277            version: into_rope(fields.version),
278            file: into_rope(fields.file),
279            sources: into_rope(fields.sources).map(SourcesField::Raw),
280            source_root: into_rope(fields.source_root),
281            sources_content: into_rope(fields.sources_content).map(SourcesContentField::Raw),
282            sections: into_rope(fields.sections),
283            names: into_rope(fields.names),
284            scopes: into_rope(fields.scopes),
285            range_mappings: into_rope(fields.range_mappings),
286            mappings: into_rope(fields.mappings),
287            ignore_list: into_rope(fields.ignore_list),
288            x_facebook_offsets: into_rope(fields.x_facebook_offsets),
289            x_metro_module_paths: into_rope(fields.x_metro_module_paths),
290            x_facebook_sources: into_rope(fields.x_facebook_sources),
291            debug_id_old: into_rope(fields.debug_id_old),
292            debug_id: into_rope(fields.debug_id),
293        })
294    }
295
296    /// Builds directly from a value's [`serde::Serialize`] implementation (e.g.
297    /// `swc_sourcemap::lazy::RawSourceMap`), capturing each top-level field's serialized JSON
298    /// value without materializing — and then re-parsing — the whole document. The result is
299    /// byte-equivalent to `Self::from_json_slice(&serde_json::to_vec(value)?)`.
300    ///
301    /// Errors if the value does not serialize as a struct or contains a field this type does
302    /// not know about; callers should fall back to serializing and [`Self::from_json`] then.
303    pub fn from_serialize<T: serde::Serialize>(value: &T) -> Result<Self> {
304        let fields = value
305            .serialize(FieldSplit)
306            .map_err(|error| anyhow::anyhow!("failed to split source map fields: {error}"))?;
307        let mut result = StructuredSourceMap::default();
308        for (key, bytes) in fields {
309            let rope = Rope::from(bytes);
310            match key {
311                "version" => result.version = Some(rope),
312                "file" => result.file = Some(rope),
313                "sources" => result.sources = Some(SourcesField::Raw(rope)),
314                "sourceRoot" => result.source_root = Some(rope),
315                "sourcesContent" => result.sources_content = Some(SourcesContentField::Raw(rope)),
316                "sections" => result.sections = Some(rope),
317                "names" => result.names = Some(rope),
318                "scopes" => result.scopes = Some(rope),
319                "rangeMappings" => result.range_mappings = Some(rope),
320                "mappings" => result.mappings = Some(rope),
321                "ignoreList" => result.ignore_list = Some(rope),
322                "x_facebook_offsets" => result.x_facebook_offsets = Some(rope),
323                "x_metro_module_paths" => result.x_metro_module_paths = Some(rope),
324                "x_facebook_sources" => result.x_facebook_sources = Some(rope),
325                "debug_id" => result.debug_id_old = Some(rope),
326                "debugId" => result.debug_id = Some(rope),
327                other => anyhow::bail!("unknown source map field `{other}`"),
328            }
329        }
330        Ok(result)
331    }
332
333    /// Rewrites each `sources` entry, keeping every other field (including the shared
334    /// `sourcesContent` ropes) intact. `rewrite` returns `None` to leave an entry unchanged.
335    ///
336    /// `sources` is only decoded if a rewrite actually changes an entry; if it cannot be decoded
337    /// (e.g. non-string entries in an external map) the map is returned unchanged.
338    pub fn rewrite_sources(
339        &self,
340        mut rewrite: impl FnMut(&str) -> Result<Option<String>>,
341    ) -> Result<Self> {
342        let mut result = self.clone();
343        let mut sources: Vec<Option<String>> = match &self.sources {
344            None => return Ok(result),
345            Some(SourcesField::Rewritten(sources)) => sources.clone(),
346            Some(SourcesField::Raw(raw)) => match serde_json::from_slice(&raw.to_bytes()) {
347                Ok(sources) => sources,
348                Err(_) => return Ok(result),
349            },
350        };
351        let mut changed = false;
352        for source in sources.iter_mut().flatten() {
353            if let Some(new_source) = rewrite(source)? {
354                *source = new_source;
355                changed = true;
356            }
357        }
358        if changed {
359            result.sources = Some(SourcesField::Rewritten(sources));
360        }
361        Ok(result)
362    }
363
364    /// Serializes the map. Field order matches `swc_sourcemap`'s serializer; `sourcesContent`
365    /// and `mappings` ropes are shared, not copied, into the result.
366    pub fn to_rope(&self) -> Rope {
367        // `key` is a literal identifier and `raw_json` holds a complete, valid JSON value
368        // (parsed as `RawValue` or produced by `serde_json` serialization), so neither needs
369        // escaping here.
370        fn field(
371            builder: &mut RopeBuilder,
372            first: &mut bool,
373            key: &'static str,
374            raw_json: Option<&Rope>,
375        ) {
376            if let Some(value) = raw_json {
377                if !*first {
378                    *builder += ",";
379                }
380                *first = false;
381                *builder += "\"";
382                *builder += key;
383                *builder += "\":";
384                *builder += value;
385            }
386        }
387
388        let mut builder = RopeBuilder::default();
389        let mut first = true;
390        builder += "{";
391        field(&mut builder, &mut first, "version", self.version.as_ref());
392        field(&mut builder, &mut first, "file", self.file.as_ref());
393        match &self.sources {
394            None => {}
395            Some(SourcesField::Raw(raw)) => {
396                field(&mut builder, &mut first, "sources", Some(raw));
397            }
398            Some(SourcesField::Rewritten(sources)) => {
399                if !first {
400                    builder += ",";
401                }
402                first = false;
403                builder += "\"sources\":";
404                builder += &Rope::from(
405                    serde_json::to_vec(sources).expect("string serialization is infallible"),
406                );
407            }
408        }
409        field(
410            &mut builder,
411            &mut first,
412            "sourceRoot",
413            self.source_root.as_ref(),
414        );
415        match &self.sources_content {
416            None => {}
417            Some(SourcesContentField::Raw(raw)) => {
418                field(&mut builder, &mut first, "sourcesContent", Some(raw));
419            }
420            Some(SourcesContentField::Escaped(contents)) => {
421                if !first {
422                    builder += ",";
423                }
424                first = false;
425                builder += "\"sourcesContent\":[";
426                for (i, content) in contents.iter().enumerate() {
427                    if i > 0 {
428                        builder += ",";
429                    }
430                    builder += content;
431                }
432                builder += "]";
433            }
434        }
435        field(&mut builder, &mut first, "sections", self.sections.as_ref());
436        field(&mut builder, &mut first, "names", self.names.as_ref());
437        field(&mut builder, &mut first, "scopes", self.scopes.as_ref());
438        field(
439            &mut builder,
440            &mut first,
441            "rangeMappings",
442            self.range_mappings.as_ref(),
443        );
444        field(&mut builder, &mut first, "mappings", self.mappings.as_ref());
445        field(
446            &mut builder,
447            &mut first,
448            "ignoreList",
449            self.ignore_list.as_ref(),
450        );
451        field(
452            &mut builder,
453            &mut first,
454            "x_facebook_offsets",
455            self.x_facebook_offsets.as_ref(),
456        );
457        field(
458            &mut builder,
459            &mut first,
460            "x_metro_module_paths",
461            self.x_metro_module_paths.as_ref(),
462        );
463        field(
464            &mut builder,
465            &mut first,
466            "x_facebook_sources",
467            self.x_facebook_sources.as_ref(),
468        );
469        field(
470            &mut builder,
471            &mut first,
472            "debug_id",
473            self.debug_id_old.as_ref(),
474        );
475        field(&mut builder, &mut first, "debugId", self.debug_id.as_ref());
476        builder += "}";
477        builder.build()
478    }
479}
480
481impl DeterministicHash for StructuredSourceMap {
482    fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
483        // Hashing the serialized form covers every field.
484        self.to_rope().deterministic_hash(state);
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    /// `to_rope(from_swc_map(m))` must be byte-identical to `m.to_writer()` — this pins the
493    /// emitter to `swc_sourcemap`'s format so emitted maps (and snapshot fixtures) don't change.
494    #[test]
495    fn golden_matches_swc_serializer() -> Result<()> {
496        let mut builder = swc_sourcemap::SourceMapBuilder::new(None);
497        let src_a = builder.add_source("turbopack:///[project]/a.js".into());
498        let src_b = builder.add_source("turbopack:///[project]/dir/b\u{2028}c\"d\\e.js".into());
499        builder.set_source_contents(
500            src_a,
501            Some("let a = 1;\nconsole.log(\"hi\\n\", '\u{1F980}', `\u{2028}\u{0000}`);".into()),
502        );
503        builder.set_source_contents(src_b, None);
504        let name = builder.add_name("console".into());
505        builder.add_raw(0, 0, 0, 0, Some(src_a), Some(name), false);
506        builder.add_raw(0, 10, 1, 0, Some(src_b), None, false);
507        let map = builder.into_sourcemap();
508
509        let mut expected = Vec::new();
510        map.clone().to_writer(&mut expected)?;
511
512        let structured = StructuredSourceMap::from_swc_map(map)?;
513        assert_eq!(structured.to_rope().to_bytes().as_ref(), &expected[..]);
514        Ok(())
515    }
516
517    /// Same, for a map without any source contents.
518    #[test]
519    fn golden_matches_swc_serializer_no_contents() -> Result<()> {
520        let mut builder = swc_sourcemap::SourceMapBuilder::new(None);
521        let src = builder.add_source("turbopack:///[project]/a.js".into());
522        builder.add_raw(0, 0, 0, 0, Some(src), None, false);
523        let map = builder.into_sourcemap();
524
525        let mut expected = Vec::new();
526        map.clone().to_writer(&mut expected)?;
527
528        let structured = StructuredSourceMap::from_swc_map(map)?;
529        assert_eq!(structured.to_rope().to_bytes().as_ref(), &expected[..]);
530        Ok(())
531    }
532
533    /// `from_json` → `to_rope` round-trips maps produced by serde-style serializers.
534    #[test]
535    fn roundtrips_serialized_map() -> Result<()> {
536        let input = r#"{"version":3,"sources":["a.js",null],"sourcesContent":["let a = \"x\";\n",null],"names":["a"],"mappings":"AAAA"}"#;
537        let structured = StructuredSourceMap::from_json(&Rope::from(input))?;
538        assert_eq!(structured.to_rope().to_bytes().as_ref(), input.as_bytes());
539        Ok(())
540    }
541
542    /// External maps may contain JSON that does not decode into Rust strings — lone surrogate
543    /// escapes (produced by transpilers slicing source text mid-code-point) or non-string
544    /// entries. These passed through the pre-structured pipeline verbatim and must not become
545    /// parse errors or be rewritten.
546    #[test]
547    fn tolerates_undecodable_sources_and_contents() -> Result<()> {
548        let input = r#"{"version":3,"sources":["a.js",7],"sourcesContent":["x\ud800y",42,null],"mappings":"AAAA"}"#;
549        let structured = StructuredSourceMap::from_json(&Rope::from(input))?;
550        assert_eq!(structured.to_rope().to_bytes().as_ref(), input.as_bytes());
551        Ok(())
552    }
553
554    /// Non-canonical (but valid) escaping in external maps must round-trip byte-for-byte.
555    #[test]
556    fn preserves_noncanonical_escaping() -> Result<()> {
557        let input =
558            r#"{"version":3,"sources":["a\/b.js"],"sourcesContent":["c\/dé"],"mappings":"AAAA"}"#;
559        let structured = StructuredSourceMap::from_json(&Rope::from(input))?;
560        assert_eq!(structured.to_rope().to_bytes().as_ref(), input.as_bytes());
561        Ok(())
562    }
563
564    /// A rewrite over a map whose `sources` cannot be decoded leaves the map untouched instead
565    /// of failing, matching the previous rewriters which silently skipped unparsable maps.
566    #[test]
567    fn rewrite_leaves_undecodable_sources_untouched() -> Result<()> {
568        let input =
569            r#"{"version":3,"sources":[{"weird":1}],"sourcesContent":["text"],"mappings":"AAAA"}"#;
570        let structured = StructuredSourceMap::from_json(&Rope::from(input))?;
571        let rewritten = structured.rewrite_sources(|_| Ok(Some("nope".to_string())))?;
572        assert_eq!(rewritten.to_rope().to_bytes().as_ref(), input.as_bytes());
573        Ok(())
574    }
575
576    /// A rewrite that changes nothing must keep the map byte-identical (the verbatim `sources`
577    /// bytes are retained rather than re-serialized).
578    #[test]
579    fn noop_rewrite_is_byte_identical() -> Result<()> {
580        let input =
581            r#"{"version":3,"sources":["a\/b.js"],"sourcesContent":["text"],"mappings":"AAAA"}"#;
582        let structured = StructuredSourceMap::from_json(&Rope::from(input))?;
583        let rewritten = structured.rewrite_sources(|_| Ok(None))?;
584        assert_eq!(rewritten.to_rope().to_bytes().as_ref(), input.as_bytes());
585        Ok(())
586    }
587
588    /// `from_serialize` must produce exactly what serializing the value and parsing it back
589    /// would — including undecodable raw values that swc's lazy decoder passes through.
590    #[test]
591    fn from_serialize_matches_serialized_roundtrip() -> Result<()> {
592        let cases: &[&str] = &[
593            r#"{"version":3,"sources":["a.js",7],"sourcesContent":["x\ud800y",42,null],"names":["n"],"mappings":"AAAA"}"#,
594            r#"{"version":3,"sources":[],"mappings":""}"#,
595            r#"{"version":3,"file":"out.js","sources":["a\/b.js"],"sourceRoot":"","sourcesContent":["c\/d"],"names":[],"mappings":"AAAA;;AACA","ignoreList":[0]}"#,
596        ];
597        for case in cases {
598            let lazy = swc_sourcemap::lazy::decode(case.as_bytes())?.into_source_map()?;
599            let raw = lazy.into_raw_sourcemap();
600            let expected = serde_json::to_vec(&raw)?;
601            let split = StructuredSourceMap::from_serialize(&raw)?;
602            assert_eq!(
603                split.to_rope().to_bytes().as_ref(),
604                &expected[..],
605                "case: {case}"
606            );
607            let reparsed = StructuredSourceMap::from_json_slice(&expected)?;
608            assert_eq!(split, reparsed, "field-level equality, case: {case}");
609        }
610        Ok(())
611    }
612
613    #[test]
614    fn rewrite_sources_keeps_contents_shared() -> Result<()> {
615        let input = r#"{"version":3,"sources":["turbopack:///[project]/a.js"],"sourcesContent":["text"],"mappings":"AAAA"}"#;
616        let structured = StructuredSourceMap::from_json(&Rope::from(input))?;
617        let rewritten = structured.rewrite_sources(|source| {
618            Ok(source
619                .strip_prefix("turbopack:///[project]/")
620                .map(|rest| format!("file:///root/{rest}")))
621        })?;
622        let json: serde_json::Value = serde_json::from_slice(&rewritten.to_rope().to_bytes())?;
623        assert_eq!(json["sources"][0], "file:///root/a.js");
624        assert_eq!(json["sourcesContent"][0], "text");
625        Ok(())
626    }
627}