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: 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        // 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 = 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            // There was an error resolving the source map
98            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            // There is no source map for this file or no mapping for the line
108            write!(
109                writable,
110                "{PADDING}{}",
111                formatting_mode.lowlight(&format_args!("[at {original_frame}]"))
112            )?;
113        }
114        Ok(ResolvedSourceMapping::Mapped { frame }) => {
115            // There is a mapping to something outside of the project (e. g. plugins,
116            // internal code)
117            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            // There is a mapping to a file in the project directory, but to library code
132            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            // There is a mapping to a file in the project directory
148            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}