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