Skip to main content

turbopack_ecmascript/
source_map.rs

1use std::sync::LazyLock;
2
3use anyhow::Result;
4use regex::Regex;
5use swc_core::common::comments::{Comment, CommentKind};
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
15#[turbo_tasks::value(shared)]
16#[derive(Debug, Clone)]
17pub struct InlineSourceMap {
18    /// The file path of the module containing the sourcemap data URL
19    pub origin_path: FileSystemPath,
20    /// The Base64 encoded JSON sourcemap string
21    pub source_map: RcStr,
22}
23
24#[turbo_tasks::value_impl]
25impl GenerateSourceMap for InlineSourceMap {
26    #[turbo_tasks::function]
27    pub async fn generate_source_map(&self) -> Result<Vc<FileContent>> {
28        let source_map = maybe_decode_data_url(&self.source_map);
29        if let Some(source_map) =
30            resolve_source_map_sources(source_map.as_ref(), &self.origin_path).await?
31        {
32            Ok(FileContent::Content(File::from(source_map)).cell())
33        } else {
34            Ok(FileContent::NotFound.cell())
35        }
36    }
37}
38
39const SOURCE_MAPPING_URL_PREFIX: &str = "# sourceMappingURL=";
40const SOURCE_MAPPING_URL_PREFIX_LEGACY: &str = "@ sourceMappingURL=";
41
42/// Checks if a line comment is a sourceMappingURL directive and extracts the URL.
43pub fn extract_source_mapping_url(comment: &Comment) -> Option<&str> {
44    if comment.kind != CommentKind::Line {
45        return None;
46    }
47    let text = comment.text.trim();
48    text.strip_prefix(SOURCE_MAPPING_URL_PREFIX)
49        .or_else(|| text.strip_prefix(SOURCE_MAPPING_URL_PREFIX_LEGACY))
50        .map(|url| url.trim())
51}
52
53fn maybe_decode_data_url(url: &str) -> Option<Rope> {
54    const DATA_PREAMBLE: &str = "data:application/json;base64,";
55    const DATA_PREAMBLE_CHARSET: &str = "data:application/json;charset=utf-8;base64,";
56
57    let data_b64 = if let Some(data) = url.strip_prefix(DATA_PREAMBLE) {
58        data
59    } else if let Some(data) = url.strip_prefix(DATA_PREAMBLE_CHARSET) {
60        data
61    } else {
62        return None;
63    };
64
65    data_encoding::BASE64
66        .decode(data_b64.as_bytes())
67        .ok()
68        .map(Rope::from)
69}
70
71/// Extracts the sourceMappingURL from raw file content.
72/// This searches for a comment at the end of the file (only followed by whitespace).
73pub fn extract_source_mapping_url_from_content(file_content: &str) -> Option<&str> {
74    // TODO this should use https://tc39.es/ecma426/#sec-JavaScriptExtractSourceMapURL instead
75
76    // Find a matching comment at the end of the file (only followed by whitespace)
77    static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
78        LazyLock::new(|| Regex::new(r"\n//[@#]\s*sourceMappingURL=(\S*)[\n\s]*$").unwrap());
79
80    file_content.rfind("\n//").and_then(|start| {
81        let line = &file_content[start..];
82        SOURCE_MAP_FILE_REFERENCE
83            .captures(line)
84            .map(|m| m.get(1).unwrap().as_str())
85    })
86}
87
88pub async fn parse_source_map_comment(
89    source: ResolvedVc<Box<dyn Source>>,
90    source_mapping_url: Option<&str>,
91    origin_path: &FileSystemPath,
92) -> Result<
93    Option<(
94        ResolvedVc<Box<dyn GenerateSourceMap>>,
95        Option<ResolvedVc<Box<dyn ModuleReference>>>,
96    )>,
97> {
98    if let Some(path) = source_mapping_url {
99        static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
100            Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
101        });
102        if path.ends_with(".map") {
103            let source_map_origin = origin_path.parent().join(path)?;
104            let reference = SourceMapReference::new(origin_path.clone(), source_map_origin)
105                .to_resolved()
106                .await?;
107            return Ok(Some((
108                ResolvedVc::upcast(reference),
109                Some(ResolvedVc::upcast(reference)),
110            )));
111        } else if JSON_DATA_URL_BASE64.is_match(path) {
112            return Ok(Some((
113                ResolvedVc::upcast(
114                    InlineSourceMap {
115                        origin_path: origin_path.clone(),
116                        source_map: path.into(),
117                    }
118                    .resolved_cell(),
119                ),
120                None,
121            )));
122        }
123    }
124
125    if let Some(generate_source_map) =
126        ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
127    {
128        return Ok(Some((generate_source_map, None)));
129    }
130
131    Ok(None)
132}