turbopack_ecmascript/
source_map.rs1use 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 pub origin_path: FileSystemPath,
20 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
42pub 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 {
60 url.strip_prefix(DATA_PREAMBLE_CHARSET)?
61 };
62
63 data_encoding::BASE64
64 .decode(data_b64.as_bytes())
65 .ok()
66 .map(Rope::from)
67}
68
69pub fn extract_source_mapping_url_from_content(file_content: &str) -> Option<&str> {
72 static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
76 LazyLock::new(|| Regex::new(r"\n//[@#]\s*sourceMappingURL=(\S*)[\n\s]*$").unwrap());
77
78 file_content.rfind("\n//").and_then(|start| {
79 let line = &file_content[start..];
80 SOURCE_MAP_FILE_REFERENCE
81 .captures(line)
82 .map(|m| m.get(1).unwrap().as_str())
83 })
84}
85
86pub async fn parse_source_map_comment(
87 source: ResolvedVc<Box<dyn Source>>,
88 source_mapping_url: Option<&str>,
89 origin_path: &FileSystemPath,
90) -> Result<
91 Option<(
92 ResolvedVc<Box<dyn GenerateSourceMap>>,
93 Option<ResolvedVc<Box<dyn ModuleReference>>>,
94 )>,
95> {
96 if let Some(path) = source_mapping_url {
97 static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
98 Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
99 });
100 if path.ends_with(".map") {
101 let source_map_origin = origin_path.parent().join(path)?;
102 let reference = SourceMapReference::new(origin_path.clone(), source_map_origin)
103 .to_resolved()
104 .await?;
105 return Ok(Some((
106 ResolvedVc::upcast(reference),
107 Some(ResolvedVc::upcast(reference)),
108 )));
109 } else if JSON_DATA_URL_BASE64.is_match(path) {
110 return Ok(Some((
111 ResolvedVc::upcast(
112 InlineSourceMap {
113 origin_path: origin_path.clone(),
114 source_map: path.into(),
115 }
116 .resolved_cell(),
117 ),
118 None,
119 )));
120 }
121 }
122
123 if let Some(generate_source_map) =
124 ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
125 {
126 return Ok(Some((generate_source_map, None)));
127 }
128
129 Ok(None)
130}