turbopack_core/source_map/
utils.rs1use std::collections::HashSet;
2
3use anyhow::{Context, Result};
4use const_format::concatcp;
5use once_cell::sync::Lazy;
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use turbo_tasks::{ValueToString, Vc};
9use turbo_tasks_fs::{
10 DiskFileSystem, FileContent, FileSystemPath, rope::Rope, util::uri_from_file,
11};
12
13use crate::SOURCE_URL_PROTOCOL;
14
15pub fn add_default_ignore_list(map: &mut sourcemap::SourceMap) {
16 let mut ignored_ids = HashSet::new();
17
18 for (source_id, source) in map.sources().enumerate() {
19 if source.starts_with(concatcp!(SOURCE_URL_PROTOCOL, "///[next]"))
20 || source.starts_with(concatcp!(SOURCE_URL_PROTOCOL, "///[turbopack]"))
21 || source.contains("/node_modules/")
22 {
23 ignored_ids.insert(source_id);
24 }
25 }
26
27 for ignored_id in ignored_ids {
28 map.add_to_ignore_list(ignored_id as _);
29 }
30}
31
32#[derive(Serialize, Deserialize)]
33struct SourceMapSectionOffsetJson {
34 line: u32,
35 offset: u32,
36}
37
38#[derive(Serialize, Deserialize)]
39struct SourceMapSectionItemJson {
40 offset: SourceMapSectionOffsetJson,
41 map: SourceMapJson,
42}
43
44#[derive(Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48struct SourceMapJson {
49 version: u32,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 file: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 source_root: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 sources: Option<Vec<Option<String>>>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 sources_content: Option<Vec<Option<String>>>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 names: Option<Vec<String>>,
61 mappings: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 ignore_list: Option<Vec<u32>>,
64
65 debug_id: Option<String>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
69 sections: Option<Vec<SourceMapSectionItemJson>>,
70}
71
72pub async fn resolve_source_map_sources(
75 map: Option<&Rope>,
76 origin: Vc<FileSystemPath>,
77) -> Result<Option<Rope>> {
78 async fn resolve_source(
79 original_source: &mut String,
80 original_content: &mut Option<String>,
81 origin: Vc<FileSystemPath>,
82 ) -> Result<()> {
83 if let Some(path) = *origin
84 .parent()
85 .try_join((&**original_source).into())
86 .await?
87 {
88 let path_str = path.to_string().await?;
89 let source = format!("{SOURCE_URL_PROTOCOL}///{path_str}");
90 *original_source = source;
91
92 if original_content.is_none() {
93 if let FileContent::Content(file) = &*path.read().await? {
94 let text = file.content().to_str()?;
95 *original_content = Some(text.to_string())
96 } else {
97 *original_content = Some(format!("unable to read source {path_str}"));
98 }
99 }
100 } else {
101 let origin_str = origin.to_string().await?;
102 static INVALID_REGEX: Lazy<Regex> =
103 Lazy::new(|| Regex::new(r#"(?:^|/)(?:\.\.?(?:/|$))+"#).unwrap());
104 let source = INVALID_REGEX.replace_all(original_source, |s: ®ex::Captures<'_>| {
105 s[0].replace('.', "_")
106 });
107 *original_source = format!("{SOURCE_URL_PROTOCOL}///{origin_str}/{source}");
108 if original_content.is_none() {
109 *original_content = Some(format!(
110 "unable to access {original_source} in {origin_str} (it's leaving the \
111 filesystem root)"
112 ));
113 }
114 }
115 anyhow::Ok(())
116 }
117
118 async fn resolve_map(map: &mut SourceMapJson, origin: Vc<FileSystemPath>) -> Result<()> {
119 if let Some(sources) = &mut map.sources {
120 let mut contents = if let Some(mut contents) = map.sources_content.take() {
121 contents.resize(sources.len(), None);
122 contents
123 } else {
124 Vec::with_capacity(sources.len())
125 };
126
127 for (source, content) in sources.iter_mut().zip(contents.iter_mut()) {
128 if let Some(source) = source {
129 resolve_source(source, content, origin).await?;
130 }
131 }
132
133 map.sources_content = Some(contents);
134 }
135 Ok(())
136 }
137
138 let Some(map) = map else {
139 return Ok(None);
140 };
141
142 let Ok(mut map): serde_json::Result<SourceMapJson> = serde_json::from_reader(map.read()) else {
143 return Ok(None);
145 };
146
147 resolve_map(&mut map, origin).await?;
148 for section in map.sections.iter_mut().flatten() {
149 resolve_map(&mut section.map, origin).await?;
150 }
151
152 let map = Rope::from(serde_json::to_vec(&map)?);
153 Ok(Some(map))
154}
155
156pub async fn fileify_source_map(
159 map: Option<&Rope>,
160 context_path: Vc<FileSystemPath>,
161) -> Result<Option<Rope>> {
162 let Some(map) = map else {
163 return Ok(None);
164 };
165
166 let Ok(mut map): serde_json::Result<SourceMapJson> = serde_json::from_reader(map.read()) else {
167 return Ok(None);
169 };
170
171 let context_fs = context_path.fs();
172 let context_fs = &*Vc::try_resolve_downcast_type::<DiskFileSystem>(context_fs)
173 .await?
174 .context("Expected the chunking context to have a DiskFileSystem")?
175 .await?;
176 let prefix = format!("{}///[{}]/", SOURCE_URL_PROTOCOL, context_fs.name());
177
178 let transform_source = async |src: &mut Option<String>| {
179 if let Some(src) = src {
180 if let Some(src_rest) = src.strip_prefix(&prefix) {
181 *src = uri_from_file(context_path, Some(src_rest)).await?;
182 }
183 }
184 anyhow::Ok(())
185 };
186
187 for src in map.sources.iter_mut().flatten() {
188 transform_source(src).await?;
189 }
190 for section in map.sections.iter_mut().flatten() {
191 for src in section.map.sources.iter_mut().flatten() {
192 transform_source(src).await?;
193 }
194 }
195
196 let map = Rope::from(serde_json::to_vec(&map)?);
197
198 Ok(Some(map))
199}