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