turbopack_core/source_map/
mod.rs

1use std::{borrow::Cow, io::Write, ops::Deref, sync::Arc};
2
3use anyhow::Result;
4use bytes_str::BytesStr;
5use once_cell::sync::Lazy;
6use ref_cast::RefCast;
7use regex::Regex;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use swc_sourcemap::{DecodedMap, SourceMap as RegularMap, SourceMapBuilder, SourceMapIndex};
10use turbo_rcstr::{RcStr, rcstr};
11use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
12use turbo_tasks_fs::{
13    File, FileContent, FileSystem, FileSystemPath, VirtualFileSystem,
14    rope::{Rope, RopeBuilder},
15};
16
17use crate::{
18    SOURCE_URL_PROTOCOL, asset::AssetContent, source::Source,
19    source_map::utils::add_default_ignore_list, source_pos::SourcePos,
20    virtual_source::VirtualSource,
21};
22
23pub(crate) mod source_map_asset;
24pub mod utils;
25
26pub use source_map_asset::SourceMapAsset;
27
28/// Represents an empty value in a u32 variable in the sourcemap crate.
29static SOURCEMAP_CRATE_NONE_U32: u32 = !0;
30
31#[turbo_tasks::value(transparent)]
32pub struct OptionStringifiedSourceMap(Option<Rope>);
33
34#[turbo_tasks::value_impl]
35impl OptionStringifiedSourceMap {
36    #[turbo_tasks::function]
37    pub fn none() -> Vc<Self> {
38        Vc::cell(None)
39    }
40}
41
42/// Allows callers to generate source maps.
43#[turbo_tasks::value_trait]
44pub trait GenerateSourceMap {
45    /// Generates a usable source map, capable of both tracing and stringifying.
46    #[turbo_tasks::function]
47    fn generate_source_map(self: Vc<Self>) -> Vc<OptionStringifiedSourceMap>;
48
49    /// Returns an individual section of the larger source map, if found.
50    #[turbo_tasks::function]
51    fn by_section(self: Vc<Self>, _section: RcStr) -> Vc<OptionStringifiedSourceMap> {
52        Vc::cell(None)
53    }
54}
55
56/// Implements the source map specification as either a decoded or sectioned sourcemap.
57///
58/// - A "decoded" map represents a source map as if it was came out of a JSON
59/// decode.
60/// - A "sectioned" source map is a tree of many [SourceMap]
61/// covering regions of an output file.
62///
63/// The distinction between the source map spec's [sourcemap::Index] and our
64/// [SourceMap::Sectioned] is whether the sections are represented with Vcs
65/// pointers.
66#[turbo_tasks::value(shared, cell = "new", eq = "manual")]
67#[derive(Debug)]
68pub struct SourceMap {
69    /// A decoded source map contains no Vcs.
70    #[turbo_tasks(trace_ignore)]
71    map: Arc<CrateMapWrapper>,
72}
73impl Eq for SourceMap {}
74impl PartialEq for SourceMap {
75    fn eq(&self, other: &Self) -> bool {
76        Arc::ptr_eq(&self.map, &other.map)
77    }
78}
79
80#[turbo_tasks::value(transparent)]
81pub struct OptionSourceMap(Option<SourceMap>);
82
83#[turbo_tasks::value_impl]
84impl OptionSourceMap {
85    #[turbo_tasks::function]
86    pub fn none() -> Vc<Self> {
87        Vc::cell(None)
88    }
89}
90
91impl OptionSourceMap {
92    pub fn none_resolved() -> ResolvedVc<Self> {
93        ResolvedVc::cell(None)
94    }
95}
96#[turbo_tasks::value(transparent)]
97#[derive(Clone, Debug)]
98pub struct Tokens(Vec<Token>);
99
100/// A token represents a mapping in a source map.
101///
102/// It may either be Synthetic, meaning it was generated by some build tool and doesn't
103/// represent a location in a user-authored source file, or it is Original, meaning it represents a
104/// real location in source file.
105#[turbo_tasks::value]
106#[derive(Clone, Debug)]
107pub enum Token {
108    Synthetic(SyntheticToken),
109    Original(OriginalToken),
110}
111
112#[turbo_tasks::value]
113#[derive(Clone, Debug)]
114pub struct TokenWithSource {
115    pub token: Token,
116    pub source_content: Option<ResolvedVc<Box<dyn Source>>>,
117}
118
119/// A SyntheticToken represents a region of the generated file that was created
120/// by some build tool.
121#[turbo_tasks::value]
122#[derive(Clone, Debug)]
123pub struct SyntheticToken {
124    pub generated_line: u32,
125    pub generated_column: u32,
126    pub guessed_original_file: Option<RcStr>,
127}
128
129/// An OriginalToken represents a region of the generated file that exists in
130/// user-authored source file.
131#[turbo_tasks::value]
132#[derive(Clone, Debug)]
133pub struct OriginalToken {
134    pub generated_line: u32,
135    pub generated_column: u32,
136    pub original_file: RcStr,
137    pub original_line: u32,
138    pub original_column: u32,
139    pub name: Option<RcStr>,
140}
141
142impl Token {
143    pub fn generated_line(&self) -> u32 {
144        match self {
145            Self::Original(t) => t.generated_line,
146            Self::Synthetic(t) => t.generated_line,
147        }
148    }
149
150    pub fn generated_column(&self) -> u32 {
151        match self {
152            Self::Original(t) => t.generated_column,
153            Self::Synthetic(t) => t.generated_column,
154        }
155    }
156}
157
158impl From<swc_sourcemap::Token<'_>> for Token {
159    fn from(t: swc_sourcemap::Token) -> Self {
160        if t.has_source() {
161            Token::Original(OriginalToken {
162                generated_line: t.get_dst_line(),
163                generated_column: t.get_dst_col(),
164                original_file: RcStr::from(
165                    t.get_source()
166                        .expect("already checked token has source")
167                        .clone(),
168                ),
169                original_line: t.get_src_line(),
170                original_column: t.get_src_col(),
171                name: t.get_name().cloned().map(RcStr::from),
172            })
173        } else {
174            Token::Synthetic(SyntheticToken {
175                generated_line: t.get_dst_line(),
176                generated_column: t.get_dst_col(),
177                guessed_original_file: None,
178            })
179        }
180    }
181}
182
183impl TryInto<swc_sourcemap::RawToken> for Token {
184    type Error = std::num::ParseIntError;
185
186    fn try_into(self) -> Result<swc_sourcemap::RawToken, Self::Error> {
187        Ok(match self {
188            Self::Original(t) => swc_sourcemap::RawToken {
189                dst_col: t.generated_column,
190                dst_line: t.generated_line,
191                name_id: match t.name {
192                    None => SOURCEMAP_CRATE_NONE_U32,
193                    Some(name) => name.parse()?,
194                },
195                src_col: t.original_column,
196                src_line: t.original_line,
197                src_id: t.original_file.parse()?,
198                is_range: false,
199            },
200            Self::Synthetic(t) => swc_sourcemap::RawToken {
201                dst_col: t.generated_column,
202                dst_line: t.generated_line,
203                name_id: SOURCEMAP_CRATE_NONE_U32,
204                src_col: SOURCEMAP_CRATE_NONE_U32,
205                src_line: SOURCEMAP_CRATE_NONE_U32,
206                src_id: SOURCEMAP_CRATE_NONE_U32,
207                is_range: false,
208            },
209        })
210    }
211}
212
213impl SourceMap {
214    /// Creates a new SourceMap::Decoded Vc out of a [RegularMap] instance.
215    fn new_regular(map: RegularMap) -> Self {
216        Self::new_decoded(DecodedMap::Regular(map))
217    }
218
219    /// Creates a new SourceMap::Decoded Vc out of a [DecodedMap] instance.
220    fn new_decoded(map: DecodedMap) -> Self {
221        SourceMap {
222            map: Arc::new(CrateMapWrapper(map)),
223        }
224    }
225
226    pub fn new_from_rope(content: &Rope) -> Result<Option<Self>> {
227        let Ok(map) = DecodedMap::from_reader(content.read()) else {
228            return Ok(None);
229        };
230        Ok(Some(SourceMap::new_decoded(map)))
231    }
232}
233
234#[turbo_tasks::value_impl]
235impl SourceMap {
236    /// This function should be used sparingly to reduce memory usage, only in cold code paths
237    /// (issue resolving, etc).
238    #[turbo_tasks::function]
239    pub async fn new_from_rope_cached(
240        content: Vc<OptionStringifiedSourceMap>,
241    ) -> Result<Vc<OptionSourceMap>> {
242        let Some(content) = &*content.await? else {
243            return Ok(OptionSourceMap::none());
244        };
245        Ok(Vc::cell(SourceMap::new_from_rope(content)?))
246    }
247}
248
249impl SourceMap {
250    pub fn to_source_map(&self) -> Arc<CrateMapWrapper> {
251        self.map.clone()
252    }
253}
254
255static EMPTY_SOURCE_MAP_ROPE: Lazy<Rope> =
256    Lazy::new(|| Rope::from(r#"{"version":3,"sources":[],"names":[],"mappings":"A"}"#));
257
258impl SourceMap {
259    /// A source map that contains no actual source location information (no
260    /// `sources`, no mappings that point into a source). This is used to tell
261    /// Chrome that the generated code starting at a particular offset is no
262    /// longer part of the previous section's mappings.
263    pub fn empty() -> Self {
264        let mut builder = SourceMapBuilder::new(None);
265        builder.add(0, 0, 0, 0, None, None, false);
266        SourceMap::new_regular(builder.into_sourcemap())
267    }
268
269    /// A source map that contains no actual source location information (no
270    /// `sources`, no mappings that point into a source). This is used to tell
271    /// Chrome that the generated code starting at a particular offset is no
272    /// longer part of the previous section's mappings.
273    pub fn empty_rope() -> Rope {
274        EMPTY_SOURCE_MAP_ROPE.clone()
275    }
276
277    pub fn sections_to_rope(
278        sections: impl IntoIterator<Item = (SourcePos, Rope)>,
279        debug_id: Option<RcStr>,
280    ) -> Rope {
281        let mut sections = sections.into_iter().peekable();
282
283        let mut first = sections.next();
284        if let Some((offset, map)) = &mut first
285            && sections.peek().is_none()
286            && *offset == (0, 0)
287            && debug_id.is_none()
288        {
289            // There is just a single sourcemap that starts at the beginning of the file.
290            return std::mem::take(map);
291        }
292
293        // My kingdom for a decent dedent macro with interpolation!
294        // NOTE: The empty `sources` array is technically incorrect, but there is a bug
295        // in Node.js that requires sectioned source maps to have a `sources` array.
296        let mut rope = RopeBuilder::from(
297            r#"{
298  "version": 3,
299  "sources": [],
300"#,
301        );
302        if let Some(debug_id) = debug_id {
303            writeln!(rope, r#"  "debugId": "{debug_id}","#).unwrap();
304        }
305        rope += "  \"sections\": [";
306
307        let mut first_section = true;
308        for (offset, section_map) in first.into_iter().chain(sections) {
309            if !first_section {
310                rope += ",";
311            }
312            first_section = false;
313
314            write!(
315                rope,
316                r#"
317    {{"offset": {{"line": {}, "column": {}}}, "map": "#,
318                offset.line, offset.column,
319            )
320            .unwrap();
321
322            rope += &section_map;
323
324            rope += "}";
325        }
326
327        rope += "]";
328
329        rope += "\n}";
330
331        rope.build()
332    }
333
334    /// Stringifies the source map into JSON bytes.
335    pub fn to_rope(&self) -> Result<Rope> {
336        let mut bytes = vec![];
337        self.map.0.to_writer(&mut bytes)?;
338        Ok(Rope::from(bytes))
339    }
340
341    /// Traces a generated line/column into an mapping token representing either
342    /// synthetic code or user-authored original code.
343    pub fn lookup_token(&self, line: u32, column: u32) -> Token {
344        let (token, _) = self.lookup_token_and_source_internal(line, column, true);
345        token
346    }
347
348    /// Traces a generated line/column into an mapping token representing either
349    /// synthetic code or user-authored original code.
350    pub async fn lookup_token_and_source(&self, line: u32, column: u32) -> Result<TokenWithSource> {
351        let (token, content) = self.lookup_token_and_source_internal(line, column, true);
352        Ok(TokenWithSource {
353            token,
354            source_content: match content {
355                Some(v) => Some(v.to_resolved().await?),
356                None => None,
357            },
358        })
359    }
360
361    pub async fn with_resolved_sources(&self, origin: FileSystemPath) -> Result<Self> {
362        async fn resolve_source(
363            source_request: BytesStr,
364            source_content: Option<BytesStr>,
365            origin: FileSystemPath,
366        ) -> Result<(BytesStr, BytesStr)> {
367            Ok(
368                if let Some(path) = origin.parent().try_join(&source_request)? {
369                    let path_str = path.value_to_string().await?;
370                    let source = format!("{SOURCE_URL_PROTOCOL}///{path_str}");
371                    let source_content = if let Some(source_content) = source_content {
372                        source_content
373                    } else if let FileContent::Content(file) = &*path.read().await? {
374                        let text = file.content().to_str()?;
375                        text.to_string().into()
376                    } else {
377                        format!("unable to read source {path_str}").into()
378                    };
379                    (source.into(), source_content)
380                } else {
381                    let origin_str = origin.value_to_string().await?;
382                    static INVALID_REGEX: Lazy<Regex> =
383                        Lazy::new(|| Regex::new(r#"(?:^|/)(?:\.\.?(?:/|$))+"#).unwrap());
384                    let source = INVALID_REGEX
385                        .replace_all(&source_request, |s: &regex::Captures<'_>| {
386                            s[0].replace('.', "_")
387                        });
388                    let source = format!("{SOURCE_URL_PROTOCOL}///{origin_str}/{source}");
389                    let source_content = source_content.unwrap_or_else(|| {
390                        format!(
391                            "unable to access {source_request} in {origin_str} (it's leaving the \
392                             filesystem root)"
393                        )
394                        .into()
395                    });
396                    (source.into(), source_content)
397                },
398            )
399        }
400        async fn regular_map_with_resolved_sources(
401            map: &RegularMapWrapper,
402            origin: FileSystemPath,
403        ) -> Result<RegularMap> {
404            let map = &map.0;
405            let file = map.get_file().cloned();
406            let tokens = map.tokens().map(|t| t.get_raw_token()).collect();
407            let names = map.names().cloned().collect();
408            let count = map.get_source_count() as usize;
409            let sources = map.sources().cloned().collect::<Vec<_>>();
410            let source_contents = map
411                .source_contents()
412                .map(|s| s.cloned())
413                .collect::<Vec<_>>();
414            let mut new_sources = Vec::with_capacity(count);
415            let mut new_source_contents = Vec::with_capacity(count);
416            for (source, source_content) in sources.into_iter().zip(source_contents.into_iter()) {
417                let (source, source_content) =
418                    resolve_source(source, source_content, origin.clone()).await?;
419                new_sources.push(source);
420                new_source_contents.push(Some(source_content));
421            }
422            let mut map =
423                RegularMap::new(file, tokens, names, new_sources, Some(new_source_contents));
424
425            add_default_ignore_list(&mut map);
426
427            Ok(map)
428        }
429        async fn decoded_map_with_resolved_sources(
430            map: &CrateMapWrapper,
431            origin: FileSystemPath,
432        ) -> Result<CrateMapWrapper> {
433            Ok(CrateMapWrapper(match &map.0 {
434                DecodedMap::Regular(map) => {
435                    let map = RegularMapWrapper::ref_cast(map);
436                    DecodedMap::Regular(regular_map_with_resolved_sources(map, origin).await?)
437                }
438                DecodedMap::Index(map) => {
439                    let count = map.get_section_count() as usize;
440                    let file = map.get_file().cloned();
441                    let sections = map
442                        .sections()
443                        .filter_map(|section| {
444                            section
445                                .get_sourcemap()
446                                .map(|s| (section.get_offset(), CrateMapWrapper::ref_cast(s)))
447                        })
448                        .collect::<Vec<_>>();
449                    let sections = sections
450                        .into_iter()
451                        .map(|(offset, map)| {
452                            let origin = origin.clone();
453                            async move {
454                                Ok((
455                                    offset,
456                                    Box::pin(decoded_map_with_resolved_sources(
457                                        map,
458                                        origin.clone(),
459                                    ))
460                                    .await?,
461                                ))
462                            }
463                        })
464                        .try_join()
465                        .await?;
466                    let mut new_sections = Vec::with_capacity(count);
467                    for (offset, map) in sections {
468                        new_sections.push(swc_sourcemap::SourceMapSection::new(
469                            offset,
470                            // Urls are deprecated and we don't accept them
471                            None,
472                            Some(map.0),
473                        ));
474                    }
475                    DecodedMap::Index(SourceMapIndex::new(file, new_sections))
476                }
477                DecodedMap::Hermes(_) => {
478                    todo!("hermes source maps are not implemented");
479                }
480            }))
481        }
482
483        let map = Box::pin(decoded_map_with_resolved_sources(&self.map, origin)).await?;
484        Ok(Self::new_decoded(map.0))
485    }
486}
487
488#[turbo_tasks::function]
489fn sourcemap_content_fs_root() -> Vc<FileSystemPath> {
490    VirtualFileSystem::new_with_name(rcstr!("sourcemap-content")).root()
491}
492
493#[turbo_tasks::function]
494async fn sourcemap_content_source(path: RcStr, content: RcStr) -> Result<Vc<Box<dyn Source>>> {
495    let path = sourcemap_content_fs_root().await?.join(&path)?;
496    let content = AssetContent::file(FileContent::new(File::from(content)).cell());
497    Ok(Vc::upcast(VirtualSource::new(path, content)))
498}
499
500impl SourceMap {
501    fn lookup_token_and_source_internal(
502        &self,
503        line: u32,
504        column: u32,
505        need_source_content: bool,
506    ) -> (Token, Option<Vc<Box<dyn Source>>>) {
507        let mut content: Option<Vc<Box<dyn Source>>> = None;
508
509        let token: Token = {
510            let map = &self.map;
511
512            let tok = map.lookup_token(line, column);
513            let mut token = tok.map(Token::from).unwrap_or_else(|| {
514                Token::Synthetic(SyntheticToken {
515                    generated_line: line,
516                    generated_column: column,
517                    guessed_original_file: None,
518                })
519            });
520
521            if let Token::Synthetic(SyntheticToken {
522                guessed_original_file,
523                ..
524            }) = &mut token
525                && let DecodedMap::Regular(map) = &map.0
526                && map.get_source_count() == 1
527            {
528                let source = map.sources().next().unwrap().clone();
529                *guessed_original_file = Some(RcStr::from(source));
530            }
531
532            if need_source_content
533                && content.is_none()
534                && let Some(map) = map.as_regular_source_map()
535            {
536                content = tok.and_then(|tok| {
537                    let src_id = tok.get_src_id();
538
539                    let name = map.get_source(src_id);
540                    let content = map.get_source_contents(src_id);
541
542                    let (name, content) = name.zip(content)?;
543                    Some(sourcemap_content_source(
544                        name.clone().into(),
545                        content.clone().into(),
546                    ))
547                });
548            }
549
550            token
551        };
552
553        (token, content)
554    }
555}
556
557#[turbo_tasks::value_impl]
558impl GenerateSourceMap for SourceMap {
559    #[turbo_tasks::function]
560    fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
561        Ok(Vc::cell(Some(self.to_rope()?)))
562    }
563}
564
565/// Wraps the DecodedMap struct so that it is Sync and Send.
566///
567/// # Safety
568///
569/// Must not use per line access to the SourceMap, as it is not thread safe.
570#[derive(Debug, RefCast)]
571#[repr(transparent)]
572pub struct CrateMapWrapper(DecodedMap);
573
574// Safety: DecodedMap contains a raw pointer, which isn't Send, which is
575// required to cache in a Vc. So, we have wrap it in 4 layers of cruft to do it.
576unsafe impl Send for CrateMapWrapper {}
577unsafe impl Sync for CrateMapWrapper {}
578
579/// Wraps the RegularMap struct so that it is Sync and Send.
580///
581/// # Safety
582///
583/// Must not use per line access to the SourceMap, as it is not thread safe.
584#[derive(Debug, RefCast)]
585#[repr(transparent)]
586pub struct RegularMapWrapper(RegularMap);
587
588// Safety: RegularMap contains a raw pointer, which isn't Send, which is
589// required to cache in a Vc. So, we have wrap it in 4 layers of cruft to do it.
590unsafe impl Send for RegularMapWrapper {}
591unsafe impl Sync for RegularMapWrapper {}
592
593impl CrateMapWrapper {
594    pub fn as_regular_source_map(&self) -> Option<Cow<'_, RegularMap>> {
595        match &self.0 {
596            DecodedMap::Regular(m) => Some(Cow::Borrowed(m)),
597            DecodedMap::Index(m) => m.flatten().map(Cow::Owned).ok(),
598            _ => None,
599        }
600    }
601}
602
603impl Deref for CrateMapWrapper {
604    type Target = DecodedMap;
605
606    fn deref(&self) -> &Self::Target {
607        &self.0
608    }
609}
610
611impl Serialize for CrateMapWrapper {
612    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
613        use serde::ser::Error;
614        let mut bytes = vec![];
615        self.0.to_writer(&mut bytes).map_err(Error::custom)?;
616        serializer.serialize_bytes(bytes.as_slice())
617    }
618}
619
620impl<'de> Deserialize<'de> for CrateMapWrapper {
621    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
622        use serde::de::Error;
623        let bytes = <&[u8]>::deserialize(deserializer)?;
624        let map = DecodedMap::from_reader(bytes).map_err(Error::custom)?;
625        Ok(CrateMapWrapper(map))
626    }
627}