turbopack_node/source_map/
mod.rs1use std::{
2 borrow::Cow,
3 fmt::Write,
4 path::{MAIN_SEPARATOR, Path},
5};
6
7use anyhow::Result;
8use const_format::concatcp;
9use once_cell::sync::Lazy;
10use regex::Regex;
11use serde::Deserialize;
12use turbo_tasks::{ReadRef, Vc};
13use turbo_tasks_fs::{
14 FileLinesContent, FileSystemPath, source_context::get_source_context, to_sys_path,
15};
16use turbopack_cli_utils::source_context::format_source_context_lines;
17use turbopack_core::{
18 PROJECT_FILESYSTEM_NAME, SOURCE_URL_PROTOCOL,
19 source_map::{GenerateSourceMap, SourceMap},
20};
21use turbopack_ecmascript::magic_identifier::unmangle_identifiers;
22
23pub use crate::source_map::trace::{StackFrame, TraceResult, trace_source_map};
24use crate::{AssetsForSourceMapping, pool::FormattingMode};
25
26pub mod trace;
27
28const MAX_CODE_FRAMES: usize = 3;
29
30pub async fn apply_source_mapping(
31 text: &'_ str,
32 assets_for_source_mapping: Vc<AssetsForSourceMapping>,
33 root: FileSystemPath,
34 project_dir: FileSystemPath,
35 formatting_mode: FormattingMode,
36) -> Result<Cow<'_, str>> {
37 static STACK_TRACE_LINE: Lazy<Regex> =
38 Lazy::new(|| Regex::new("\n at (?:(.+) \\()?(.+):(\\d+):(\\d+)\\)?").unwrap());
39
40 let mut it = STACK_TRACE_LINE.captures_iter(text).peekable();
41 if it.peek().is_none() {
42 return Ok(Cow::Borrowed(text));
43 }
44 let mut first_error = true;
45 let mut visible_code_frames = 0;
46 let mut new = String::with_capacity(text.len() * 2);
47 let mut last_match = 0;
48 for cap in it {
49 let m = cap.get(0).unwrap();
51 new.push_str(&text[last_match..m.start()]);
52 let name = cap.get(1).map(|s| s.as_str());
53 let file = cap.get(2).unwrap().as_str();
54 let line = cap.get(3).unwrap().as_str();
55 let column = cap.get(4).unwrap().as_str();
56 let line = line.parse::<u32>()?;
57 let column = column.parse::<u32>()?;
58 let frame = StackFrame {
59 name: name.map(|s| s.into()),
60 file: file.into(),
61 line: Some(line),
62 column: Some(column),
63 };
64 let resolved = resolve_source_mapping(
65 assets_for_source_mapping,
66 root.clone(),
67 project_dir.root().owned().await?,
68 &frame,
69 )
70 .await;
71 write_resolved(
72 &mut new,
73 resolved,
74 &frame,
75 &mut first_error,
76 &mut visible_code_frames,
77 formatting_mode,
78 )?;
79 last_match = m.end();
80 }
81 new.push_str(&text[last_match..]);
82 Ok(Cow::Owned(new))
83}
84
85fn write_resolved(
86 writable: &mut impl Write,
87 resolved: Result<ResolvedSourceMapping>,
88 original_frame: &StackFrame<'_>,
89 first_error: &mut bool,
90 visible_code_frames: &mut usize,
91 formatting_mode: FormattingMode,
92) -> Result<()> {
93 const PADDING: &str = "\n ";
94 match resolved {
95 Err(err) => {
96 write!(writable, "{PADDING}at {original_frame}")?;
98 if *first_error {
99 write!(writable, "{PADDING}(error resolving source map: {err})")?;
100 *first_error = false;
101 } else {
102 write!(writable, "{PADDING}(error resolving source map)")?;
103 }
104 }
105 Ok(ResolvedSourceMapping::NoSourceMap) | Ok(ResolvedSourceMapping::Unmapped) => {
106 write!(
108 writable,
109 "{PADDING}{}",
110 formatting_mode.lowlight(&format_args!("[at {original_frame}]"))
111 )?;
112 }
113 Ok(ResolvedSourceMapping::Mapped { frame }) => {
114 write!(
117 writable,
118 "{PADDING}{}",
119 formatting_mode.lowlight(&format_args!(
120 "at {} [{}]",
121 frame,
122 original_frame.with_name(None)
123 ))
124 )?;
125 }
126 Ok(ResolvedSourceMapping::MappedLibrary {
127 frame,
128 project_path,
129 }) => {
130 write!(
132 writable,
133 "{PADDING}{}",
134 formatting_mode.lowlight(&format_args!(
135 "at {} [{}]",
136 frame.with_path(&project_path.path),
137 original_frame.with_name(None)
138 ))
139 )?;
140 }
141 Ok(ResolvedSourceMapping::MappedProject {
142 frame,
143 project_path,
144 lines,
145 }) => {
146 if let Some(name) = frame.name.as_ref() {
148 write!(
149 writable,
150 "{PADDING}at {name} ({}) {}",
151 formatting_mode.highlight(&frame.with_name(None).with_path(&project_path.path)),
152 formatting_mode.lowlight(&format_args!("[{}]", original_frame.with_name(None)))
153 )?;
154 } else {
155 write!(
156 writable,
157 "{PADDING}at {} {}",
158 formatting_mode.highlight(&frame.with_path(&project_path.path)),
159 formatting_mode.lowlight(&format_args!("[{}]", original_frame.with_name(None)))
160 )?;
161 }
162 let (line, column) = frame.get_pos().unwrap_or((0, 0));
163 let line = line.saturating_sub(1);
164 let column = column.saturating_sub(1);
165 if let FileLinesContent::Lines(lines) = &*lines
166 && *visible_code_frames < MAX_CODE_FRAMES
167 {
168 let lines = lines.iter().map(|l| l.content.as_str());
169 let ctx = get_source_context(lines, line, column, line, column);
170 match formatting_mode {
171 FormattingMode::Plain => {
172 write!(writable, "\n{ctx}")?;
173 }
174 FormattingMode::AnsiColors => {
175 writable.write_char('\n')?;
176 format_source_context_lines(&ctx, writable);
177 }
178 }
179 *visible_code_frames += 1;
180 }
181 }
182 }
183 Ok(())
184}
185
186enum ResolvedSourceMapping {
187 NoSourceMap,
188 Unmapped,
189 Mapped {
190 frame: StackFrame<'static>,
191 },
192 MappedProject {
193 frame: StackFrame<'static>,
194 project_path: FileSystemPath,
195 lines: ReadRef<FileLinesContent>,
196 },
197 MappedLibrary {
198 frame: StackFrame<'static>,
199 project_path: FileSystemPath,
200 },
201}
202
203async fn resolve_source_mapping(
204 assets_for_source_mapping: Vc<AssetsForSourceMapping>,
205 root: FileSystemPath,
206 project_dir: FileSystemPath,
207 frame: &StackFrame<'_>,
208) -> Result<ResolvedSourceMapping> {
209 let Some((line, column)) = frame.get_pos() else {
210 return Ok(ResolvedSourceMapping::NoSourceMap);
211 };
212 let name = frame.name.as_ref();
213 let file = &frame.file;
214 let Some(root) = to_sys_path(root).await? else {
215 return Ok(ResolvedSourceMapping::NoSourceMap);
216 };
217 let Ok(file) = Path::new(file.as_ref()).strip_prefix(root) else {
218 return Ok(ResolvedSourceMapping::NoSourceMap);
219 };
220 let file = file.to_string_lossy();
221 let file = if MAIN_SEPARATOR != '/' {
222 Cow::Owned(file.replace(MAIN_SEPARATOR, "/"))
223 } else {
224 file
225 };
226 let map = assets_for_source_mapping.await?;
227 let Some(generate_source_map) = map.get(file.as_ref()) else {
228 return Ok(ResolvedSourceMapping::NoSourceMap);
229 };
230 let sm = generate_source_map.generate_source_map();
231 let Some(sm) = &*SourceMap::new_from_rope_cached(sm).await? else {
232 return Ok(ResolvedSourceMapping::NoSourceMap);
233 };
234 let trace = trace_source_map(sm, line, column, name.map(|s| &**s));
235 match trace {
236 TraceResult::Found(frame) => {
237 let lib_code = frame.file.contains("/node_modules/");
238 if let Some(project_path) = frame.file.strip_prefix(concatcp!(
239 SOURCE_URL_PROTOCOL,
240 "///[",
241 PROJECT_FILESYSTEM_NAME,
242 "]/"
243 )) {
244 let fs_path = project_dir.join(project_path)?;
245 if lib_code {
246 return Ok(ResolvedSourceMapping::MappedLibrary {
247 frame: frame.clone(),
248 project_path: fs_path.clone(),
249 });
250 } else {
251 let lines = fs_path.read().lines().await?;
252 return Ok(ResolvedSourceMapping::MappedProject {
253 frame: frame.clone(),
254 project_path: fs_path.clone(),
255 lines,
256 });
257 }
258 }
259 Ok(ResolvedSourceMapping::Mapped {
260 frame: frame.clone(),
261 })
262 }
263 TraceResult::NotFound => Ok(ResolvedSourceMapping::Unmapped),
264 }
265}
266
267#[turbo_tasks::value(shared)]
268#[derive(Clone, Debug, Deserialize)]
269pub struct StructuredError {
270 pub name: String,
271 pub message: String,
272 #[turbo_tasks(trace_ignore)]
273 stack: Vec<StackFrame<'static>>,
274 cause: Option<Box<StructuredError>>,
275}
276
277impl StructuredError {
278 pub async fn print(
279 &self,
280 assets_for_source_mapping: Vc<AssetsForSourceMapping>,
281 root: FileSystemPath,
282 root_path: FileSystemPath,
283 formatting_mode: FormattingMode,
284 ) -> Result<String> {
285 let mut message = String::new();
286
287 let magic = |content| formatting_mode.magic_identifier(content);
288
289 write!(
290 message,
291 "{}: {}",
292 self.name,
293 unmangle_identifiers(&self.message, magic)
294 )?;
295
296 let mut first_error = true;
297 let mut visible_code_frames = 0;
298
299 for frame in &self.stack {
300 let frame = frame.unmangle_identifiers(magic);
301 let resolved = resolve_source_mapping(
302 assets_for_source_mapping,
303 root.clone(),
304 root_path.clone(),
305 &frame,
306 )
307 .await;
308 write_resolved(
309 &mut message,
310 resolved,
311 &frame,
312 &mut first_error,
313 &mut visible_code_frames,
314 formatting_mode,
315 )?;
316 }
317
318 if let Some(cause) = &self.cause {
319 message.write_str("\nCaused by: ")?;
320 message.write_str(
321 &Box::pin(cause.print(
322 assets_for_source_mapping,
323 root.clone(),
324 root_path.clone(),
325 formatting_mode,
326 ))
327 .await?,
328 )?;
329 }
330
331 Ok(message)
332 }
333}