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: 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 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 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 write!(
105 writable,
106 "{PADDING}{}",
107 formatting_mode.lowlight(&format_args!("[at {original_frame}]"))
108 )?;
109 }
110 Ok(ResolvedSourceMapping::Mapped { frame }) => {
111 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 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 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}