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