turbopack_ecmascript/
source_map.rs1use 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 pub origin_path: FileSystemPath,
24 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 let source_map_comment = match comments {
68 Either::Left(comments) => {
69 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 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}