turbopack_ecmascript/
source_map.rs

1use std::sync::LazyLock;
2
3use anyhow::Result;
4use either::Either;
5use regex::Regex;
6use turbo_rcstr::RcStr;
7use turbo_tasks::{ResolvedVc, Vc};
8use turbo_tasks_fs::{FileSystemPath, rope::Rope};
9use turbopack_core::{
10    reference::{ModuleReference, SourceMapReference},
11    source::Source,
12    source_map::{
13        GenerateSourceMap, OptionStringifiedSourceMap, utils::resolve_source_map_sources,
14    },
15};
16
17use crate::swc_comments::ImmutableComments;
18
19#[turbo_tasks::value(shared)]
20#[derive(Debug, Clone)]
21pub struct InlineSourceMap {
22    /// The file path of the module containing the sourcemap data URL
23    pub origin_path: FileSystemPath,
24    /// The Base64 encoded JSON sourcemap string
25    pub source_map: RcStr,
26}
27
28#[turbo_tasks::value_impl]
29impl GenerateSourceMap for InlineSourceMap {
30    #[turbo_tasks::function]
31    pub async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
32        let source_map = maybe_decode_data_url(&self.source_map);
33        let source_map = resolve_source_map_sources(source_map.as_ref(), &self.origin_path).await?;
34        Ok(Vc::cell(source_map))
35    }
36}
37
38fn maybe_decode_data_url(url: &str) -> Option<Rope> {
39    const DATA_PREAMBLE: &str = "data:application/json;base64,";
40    const DATA_PREAMBLE_CHARSET: &str = "data:application/json;charset=utf-8;base64,";
41
42    let data_b64 = if let Some(data) = url.strip_prefix(DATA_PREAMBLE) {
43        data
44    } else if let Some(data) = url.strip_prefix(DATA_PREAMBLE_CHARSET) {
45        data
46    } else {
47        return None;
48    };
49
50    data_encoding::BASE64
51        .decode(data_b64.as_bytes())
52        .ok()
53        .map(Rope::from)
54}
55
56pub async fn parse_source_map_comment(
57    source: ResolvedVc<Box<dyn Source>>,
58    comments: Either<&ImmutableComments, &str>,
59    origin_path: &FileSystemPath,
60) -> Result<
61    Option<(
62        ResolvedVc<Box<dyn GenerateSourceMap>>,
63        Option<ResolvedVc<Box<dyn ModuleReference>>>,
64    )>,
65> {
66    // See https://tc39.es/ecma426/#sec-MatchSourceMapURL for the official regex.
67    let source_map_comment = match comments {
68        Either::Left(comments) => {
69            // Only use the last sourceMappingURL comment by spec
70            static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
71                LazyLock::new(|| Regex::new(r"[@#]\s*sourceMappingURL=(\S*)$").unwrap());
72            let mut paths_by_pos = Vec::new();
73            for (pos, comments) in comments.trailing.iter() {
74                for comment in comments.iter().rev() {
75                    if let Some(m) = SOURCE_MAP_FILE_REFERENCE.captures(&comment.text) {
76                        let path = m.get(1).unwrap().as_str();
77                        paths_by_pos.push((pos, path));
78                        break;
79                    }
80                }
81            }
82            paths_by_pos
83                .into_iter()
84                .max_by_key(|&(pos, _)| pos)
85                .map(|(_, path)| path)
86        }
87        Either::Right(file_content) => {
88            // Find a matching comment at the end of the file (only followed by whitespace)
89            static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
90                LazyLock::new(|| Regex::new(r"\n//[@#]\s*sourceMappingURL=(\S*)[\n\s]*$").unwrap());
91
92            file_content.rfind("\n//").and_then(|start| {
93                let line = &file_content[start..];
94                SOURCE_MAP_FILE_REFERENCE
95                    .captures(line)
96                    .map(|m| m.get(1).unwrap().as_str())
97            })
98        }
99    };
100
101    if let Some(path) = source_map_comment {
102        static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
103            Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
104        });
105        if path.ends_with(".map") {
106            let source_map_origin = origin_path.parent().join(path)?;
107            let reference = SourceMapReference::new(origin_path.clone(), source_map_origin)
108                .to_resolved()
109                .await?;
110            return Ok(Some((
111                ResolvedVc::upcast(reference),
112                Some(ResolvedVc::upcast(reference)),
113            )));
114        } else if JSON_DATA_URL_BASE64.is_match(path) {
115            return Ok(Some((
116                ResolvedVc::upcast(
117                    InlineSourceMap {
118                        origin_path: origin_path.clone(),
119                        source_map: path.into(),
120                    }
121                    .resolved_cell(),
122                ),
123                None,
124            )));
125        }
126    }
127
128    if let Some(generate_source_map) =
129        ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
130    {
131        return Ok(Some((generate_source_map, None)));
132    }
133
134    Ok(None)
135}