turbopack_node/source_map/
mod.rs

1use 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: Vc<FileSystemPath>,
35    project_dir: Vc<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        // unwrap on 0 is OK because captures only reports matches
51        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 =
66            resolve_source_mapping(assets_for_source_mapping, root, project_dir.root(), &frame)
67                .await;
68        write_resolved(
69            &mut new,
70            resolved,
71            &frame,
72            &mut first_error,
73            &mut visible_code_frames,
74            formatting_mode,
75        )?;
76        last_match = m.end();
77    }
78    new.push_str(&text[last_match..]);
79    Ok(Cow::Owned(new))
80}
81
82fn write_resolved(
83    writable: &mut impl Write,
84    resolved: Result<ResolvedSourceMapping>,
85    original_frame: &StackFrame<'_>,
86    first_error: &mut bool,
87    visible_code_frames: &mut usize,
88    formatting_mode: FormattingMode,
89) -> Result<()> {
90    const PADDING: &str = "\n    ";
91    match resolved {
92        Err(err) => {
93            // There was an error resolving the source map
94            write!(writable, "{PADDING}at {original_frame}")?;
95            if *first_error {
96                write!(writable, "{PADDING}(error resolving source map: {err})")?;
97                *first_error = false;
98            } else {
99                write!(writable, "{PADDING}(error resolving source map)")?;
100            }
101        }
102        Ok(ResolvedSourceMapping::NoSourceMap) | Ok(ResolvedSourceMapping::Unmapped) => {
103            // There is no source map for this file or no mapping for the line
104            write!(
105                writable,
106                "{PADDING}{}",
107                formatting_mode.lowlight(&format_args!("[at {original_frame}]"))
108            )?;
109        }
110        Ok(ResolvedSourceMapping::Mapped { frame }) => {
111            // There is a mapping to something outside of the project (e. g. plugins,
112            // internal code)
113            write!(
114                writable,
115                "{PADDING}{}",
116                formatting_mode.lowlight(&format_args!(
117                    "at {} [{}]",
118                    frame,
119                    original_frame.with_name(None)
120                ))
121            )?;
122        }
123        Ok(ResolvedSourceMapping::MappedLibrary {
124            frame,
125            project_path,
126        }) => {
127            // There is a mapping to a file in the project directory, but to library code
128            write!(
129                writable,
130                "{PADDING}{}",
131                formatting_mode.lowlight(&format_args!(
132                    "at {} [{}]",
133                    frame.with_path(&project_path.path),
134                    original_frame.with_name(None)
135                ))
136            )?;
137        }
138        Ok(ResolvedSourceMapping::MappedProject {
139            frame,
140            project_path,
141            lines,
142        }) => {
143            // There is a mapping to a file in the project directory
144            if let Some(name) = frame.name.as_ref() {
145                write!(
146                    writable,
147                    "{PADDING}at {name} ({}) {}",
148                    formatting_mode.highlight(&frame.with_name(None).with_path(&project_path.path)),
149                    formatting_mode.lowlight(&format_args!("[{}]", original_frame.with_name(None)))
150                )?;
151            } else {
152                write!(
153                    writable,
154                    "{PADDING}at {} {}",
155                    formatting_mode.highlight(&frame.with_path(&project_path.path)),
156                    formatting_mode.lowlight(&format_args!("[{}]", original_frame.with_name(None)))
157                )?;
158            }
159            let (line, column) = frame.get_pos().unwrap_or((0, 0));
160            let line = line.saturating_sub(1);
161            let column = column.saturating_sub(1);
162            if let FileLinesContent::Lines(lines) = &*lines {
163                if *visible_code_frames < MAX_CODE_FRAMES {
164                    let lines = lines.iter().map(|l| l.content.as_str());
165                    let ctx = get_source_context(lines, line, column, line, column);
166                    match formatting_mode {
167                        FormattingMode::Plain => {
168                            write!(writable, "\n{ctx}")?;
169                        }
170                        FormattingMode::AnsiColors => {
171                            writable.write_char('\n')?;
172                            format_source_context_lines(&ctx, writable);
173                        }
174                    }
175                    *visible_code_frames += 1;
176                }
177            }
178        }
179    }
180    Ok(())
181}
182
183enum ResolvedSourceMapping {
184    NoSourceMap,
185    Unmapped,
186    Mapped {
187        frame: StackFrame<'static>,
188    },
189    MappedProject {
190        frame: StackFrame<'static>,
191        project_path: ReadRef<FileSystemPath>,
192        lines: ReadRef<FileLinesContent>,
193    },
194    MappedLibrary {
195        frame: StackFrame<'static>,
196        project_path: ReadRef<FileSystemPath>,
197    },
198}
199
200async fn resolve_source_mapping(
201    assets_for_source_mapping: Vc<AssetsForSourceMapping>,
202    root: Vc<FileSystemPath>,
203    project_dir: Vc<FileSystemPath>,
204    frame: &StackFrame<'_>,
205) -> Result<ResolvedSourceMapping> {
206    let Some((line, column)) = frame.get_pos() else {
207        return Ok(ResolvedSourceMapping::NoSourceMap);
208    };
209    let name = frame.name.as_ref();
210    let file = &frame.file;
211    let Some(root) = to_sys_path(root).await? else {
212        return Ok(ResolvedSourceMapping::NoSourceMap);
213    };
214    let Ok(file) = Path::new(file.as_ref()).strip_prefix(root) else {
215        return Ok(ResolvedSourceMapping::NoSourceMap);
216    };
217    let file = file.to_string_lossy();
218    let file = if MAIN_SEPARATOR != '/' {
219        Cow::Owned(file.replace(MAIN_SEPARATOR, "/"))
220    } else {
221        file
222    };
223    let map = assets_for_source_mapping.await?;
224    let Some(generate_source_map) = map.get(file.as_ref()) else {
225        return Ok(ResolvedSourceMapping::NoSourceMap);
226    };
227    let sm = generate_source_map.generate_source_map();
228    let Some(sm) = &*SourceMap::new_from_rope_cached(sm).await? else {
229        return Ok(ResolvedSourceMapping::NoSourceMap);
230    };
231    let trace = trace_source_map(sm, line, column, name.map(|s| &**s)).await?;
232    match trace {
233        TraceResult::Found(frame) => {
234            let lib_code = frame.file.contains("/node_modules/");
235            if let Some(project_path) = frame.file.strip_prefix(concatcp!(
236                SOURCE_URL_PROTOCOL,
237                "///[",
238                PROJECT_FILESYSTEM_NAME,
239                "]/"
240            )) {
241                let fs_path = project_dir.join(project_path.into());
242                if lib_code {
243                    return Ok(ResolvedSourceMapping::MappedLibrary {
244                        frame: frame.clone(),
245                        project_path: fs_path.await?,
246                    });
247                } else {
248                    let lines = fs_path.read().lines().await?;
249                    return Ok(ResolvedSourceMapping::MappedProject {
250                        frame: frame.clone(),
251                        project_path: fs_path.await?,
252                        lines,
253                    });
254                }
255            }
256            Ok(ResolvedSourceMapping::Mapped {
257                frame: frame.clone(),
258            })
259        }
260        TraceResult::NotFound => Ok(ResolvedSourceMapping::Unmapped),
261    }
262}
263
264#[turbo_tasks::value(shared)]
265#[derive(Clone, Debug)]
266pub struct StructuredError {
267    pub name: String,
268    pub message: String,
269    #[turbo_tasks(trace_ignore)]
270    stack: Vec<StackFrame<'static>>,
271    cause: Option<Box<StructuredError>>,
272}
273
274impl StructuredError {
275    pub async fn print(
276        &self,
277        assets_for_source_mapping: Vc<AssetsForSourceMapping>,
278        root: Vc<FileSystemPath>,
279        root_path: Vc<FileSystemPath>,
280        formatting_mode: FormattingMode,
281    ) -> Result<String> {
282        let mut message = String::new();
283
284        let magic = |content| formatting_mode.magic_identifier(content);
285
286        write!(
287            message,
288            "{}: {}",
289            self.name,
290            unmangle_identifiers(&self.message, magic)
291        )?;
292
293        let mut first_error = true;
294        let mut visible_code_frames = 0;
295
296        for frame in &self.stack {
297            let frame = frame.unmangle_identifiers(magic);
298            let resolved =
299                resolve_source_mapping(assets_for_source_mapping, root, root_path, &frame).await;
300            write_resolved(
301                &mut message,
302                resolved,
303                &frame,
304                &mut first_error,
305                &mut visible_code_frames,
306                formatting_mode,
307            )?;
308        }
309
310        if let Some(cause) = &self.cause {
311            message.write_str("\nCaused by: ")?;
312            message.write_str(
313                &Box::pin(cause.print(assets_for_source_mapping, root, root_path, formatting_mode))
314                    .await?,
315            )?;
316        }
317
318        Ok(message)
319    }
320}
321
322pub async fn trace_stack(
323    error: StructuredError,
324    root_asset: Vc<Box<dyn OutputAsset>>,
325    output_path: Vc<FileSystemPath>,
326    project_dir: Vc<FileSystemPath>,
327) -> Result<String> {
328    let assets_for_source_mapping = internal_assets_for_source_mapping(root_asset, output_path);
329
330    trace_stack_with_source_mapping_assets(
331        error,
332        assets_for_source_mapping,
333        output_path,
334        project_dir,
335    )
336    .await
337}
338
339#[instrument(level = Level::TRACE, skip_all)]
340pub async fn trace_stack_with_source_mapping_assets(
341    error: StructuredError,
342    assets_for_source_mapping: Vc<AssetsForSourceMapping>,
343    output_path: Vc<FileSystemPath>,
344    project_dir: Vc<FileSystemPath>,
345) -> Result<String> {
346    error
347        .print(
348            assets_for_source_mapping,
349            output_path,
350            project_dir,
351            FormattingMode::Plain,
352        )
353        .await
354}