1use std::borrow::Cow;
6use std::collections::HashMap;
7use std::rc::Rc;
8use std::str;
9use std::sync::Arc;
10
11pub use sourcemap::SourceMap;
12
13use crate::ModuleLoader;
14use crate::ModuleName;
15use crate::resolve_url;
16
17#[derive(Debug, PartialEq)]
18pub enum SourceMapApplication {
19 Unchanged,
21 LineAndColumn {
23 line_number: u32,
24 column_number: u32,
25 },
26 LineAndColumnAndFileName {
28 file_name: String,
29 line_number: u32,
30 column_number: u32,
31 },
32}
33
34pub type SourceMapData = Cow<'static, [u8]>;
35
36pub struct SourceMapper {
37 maps: HashMap<String, Option<Arc<SourceMap>>>,
40 source_lines: HashMap<(String, i64), Option<String>>,
41
42 loader: Rc<dyn ModuleLoader>,
43
44 ext_source_maps: HashMap<ModuleName, SourceMapData>,
45 source_map_urls: HashMap<ModuleName, String>,
46}
47
48impl SourceMapper {
49 pub fn new(loader: Rc<dyn ModuleLoader>) -> Self {
50 Self {
51 maps: Default::default(),
52 source_lines: Default::default(),
53 ext_source_maps: Default::default(),
54 source_map_urls: Default::default(),
55 loader,
56 }
57 }
58
59 pub(crate) fn add_ext_source_map(
61 &mut self,
62 module_name: ModuleName,
63 source_map_data: SourceMapData,
64 ) {
65 self.ext_source_maps.insert(module_name, source_map_data);
66 }
67
68 pub(crate) fn take_ext_source_maps(
69 &mut self,
70 ) -> HashMap<ModuleName, SourceMapData> {
71 std::mem::take(&mut self.ext_source_maps)
72 }
73
74 pub(crate) fn add_source_map(
76 &mut self,
77 module_name: ModuleName,
78 source_map: SourceMap,
79 ) {
80 self
81 .maps
82 .insert(module_name.to_string(), Some(Arc::new(source_map)));
83 }
84
85 pub(crate) fn add_source_map_url(
86 &mut self,
87 module_name: ModuleName,
88 source_map_url: String,
89 ) {
90 self.source_map_urls.insert(module_name, source_map_url);
91 }
92
93 pub fn apply_source_map(
99 &mut self,
100 file_name: &str,
101 line_number: u32,
102 column_number: u32,
103 ) -> SourceMapApplication {
104 let line_number = line_number - 1;
106 let column_number = column_number - 1;
107
108 let maybe_source_map =
109 self.maps.entry(file_name.to_owned()).or_insert_with(|| {
110 None
111 .or_else(|| {
113 SourceMap::from_slice(self.ext_source_maps.get(file_name)?)
114 .ok()
115 .map(Arc::new)
116 })
117 .or_else(|| {
119 let source_map_url = self.source_map_urls.get(file_name)?;
121 let source_map_data =
123 self.loader.load_external_source_map(source_map_url)?;
124 SourceMap::from_slice(&source_map_data).ok().map(Arc::new)
125 })
126 .or_else(|| {
128 SourceMap::from_slice(&self.loader.get_source_map(file_name)?)
129 .ok()
130 .map(Arc::new)
131 })
132 });
133
134 let Some(source_map) = maybe_source_map.as_ref() else {
135 return SourceMapApplication::Unchanged;
136 };
137
138 let Some(token) = source_map.lookup_token(line_number, column_number)
139 else {
140 return SourceMapApplication::Unchanged;
141 };
142
143 let new_line_number = token.get_src_line() + 1;
144 let new_column_number = token.get_src_col() + 1;
145
146 let new_file_name = match token.get_source() {
147 Some(source_file_name) => {
148 if source_file_name == file_name {
149 None
150 } else {
151 match resolve_url(source_file_name) {
156 Ok(m) if m.scheme() == "blob" => None,
157 Ok(m) => Some(m.to_string()),
158 Err(_) => resolve_url(file_name)
159 .ok()
160 .and_then(|base_url| base_url.join(source_file_name).ok())
161 .and_then(|resolved| {
162 let resolved_str = resolved.to_string();
163 match self.loader.source_map_source_exists(&resolved_str) {
167 Some(true) => Some(resolved_str),
168 _ => None,
169 }
170 }),
171 }
172 }
173 }
174 None => None,
175 };
176
177 match new_file_name {
178 None => SourceMapApplication::LineAndColumn {
179 line_number: new_line_number,
180 column_number: new_column_number,
181 },
182 Some(file_name) => SourceMapApplication::LineAndColumnAndFileName {
183 file_name,
184 line_number: new_line_number,
185 column_number: new_column_number,
186 },
187 }
188 }
189
190 const MAX_SOURCE_LINE_LENGTH: usize = 150;
191
192 pub fn get_source_line(
193 &mut self,
194 file_name: &str,
195 line_number: i64,
196 ) -> Option<String> {
197 if let Some(maybe_source_line) =
198 self.source_lines.get(&(file_name.to_string(), line_number))
199 {
200 return maybe_source_line.clone();
201 }
202
203 let maybe_source_line = self
204 .loader
205 .get_source_mapped_source_line(file_name, (line_number - 1) as usize)
206 .filter(|s| s.len() <= Self::MAX_SOURCE_LINE_LENGTH);
207
208 self.source_lines.insert(
210 (file_name.to_string(), line_number),
211 maybe_source_line.clone(),
212 );
213 maybe_source_line
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use url::Url;
220
221 use super::*;
222 use crate::ModuleCodeString;
223 use crate::ModuleLoadReferrer;
224 use crate::ModuleLoadResponse;
225 use crate::ModuleSpecifier;
226 use crate::ResolutionKind;
227 use crate::ascii_str;
228 use crate::error::ModuleLoaderError;
229 use crate::modules::ModuleLoadOptions;
230
231 struct SourceMapLoaderContent {
232 source_map: Option<ModuleCodeString>,
233 }
234
235 #[derive(Default)]
236 pub struct SourceMapLoader {
237 map: HashMap<ModuleSpecifier, SourceMapLoaderContent>,
238 existing_files: std::cell::RefCell<std::collections::HashSet<String>>,
239 }
240
241 impl SourceMapLoader {
242 fn add_existing_file(&self, file_name: &str) {
243 self
244 .existing_files
245 .borrow_mut()
246 .insert(file_name.to_string());
247 }
248 }
249
250 impl ModuleLoader for SourceMapLoader {
251 fn resolve(
252 &self,
253 _specifier: &str,
254 _referrer: &str,
255 _kind: ResolutionKind,
256 ) -> Result<ModuleSpecifier, ModuleLoaderError> {
257 unreachable!()
258 }
259
260 fn load(
261 &self,
262 _module_specifier: &ModuleSpecifier,
263 _maybe_referrer: Option<&ModuleLoadReferrer>,
264 _options: ModuleLoadOptions,
265 ) -> ModuleLoadResponse {
266 unreachable!()
267 }
268
269 fn get_source_map(&self, file_name: &str) -> Option<Cow<'_, [u8]>> {
270 let url = Url::parse(file_name).unwrap();
271 let content = self.map.get(&url)?;
272 content
273 .source_map
274 .as_ref()
275 .map(|s| Cow::Borrowed(s.as_bytes()))
276 }
277
278 fn get_source_mapped_source_line(
279 &self,
280 _file_name: &str,
281 _line_number: usize,
282 ) -> Option<String> {
283 Some("fake source line".to_string())
284 }
285
286 fn source_map_source_exists(&self, source_url: &str) -> Option<bool> {
287 Some(self.existing_files.borrow().contains(source_url))
288 }
289 }
290
291 #[test]
292 fn test_source_mapper() {
293 let mut loader = SourceMapLoader::default();
294 loader.map.insert(
295 Url::parse("file:///b.js").unwrap(),
296 SourceMapLoaderContent { source_map: None },
297 );
298 loader.map.insert(
299 Url::parse("file:///a.ts").unwrap(),
300 SourceMapLoaderContent {
301 source_map: Some(ascii_str!(r#"{"version":3,"sources":["file:///a.ts"],"sourcesContent":["export function a(): string {\n return \"a\";\n}\n"],"names":[],"mappings":"AAAA,OAAO,SAAS;EACd,OAAO;AACT"}"#).into()),
302 },
303 );
304
305 let mut source_mapper = SourceMapper::new(Rc::new(loader));
306
307 let application =
309 source_mapper.apply_source_map("file:///doesnt_exist.js", 1, 1);
310 assert_eq!(application, SourceMapApplication::Unchanged);
311
312 let application = source_mapper.apply_source_map("file:///b.js", 1, 1);
314 assert_eq!(application, SourceMapApplication::Unchanged);
315
316 let application = source_mapper.apply_source_map("file:///a.ts", 1, 21);
318 assert_eq!(
319 application,
320 SourceMapApplication::LineAndColumn {
321 line_number: 1,
322 column_number: 17
323 }
324 );
325
326 let line = source_mapper.get_source_line("file:///a.ts", 1).unwrap();
327 assert_eq!(line, "fake source line");
328 let line = source_mapper.get_source_line("file:///a.ts", 1).unwrap();
330 assert_eq!(line, "fake source line");
331 }
332
333 #[test]
334 fn test_source_map_relative_path_nonexistent_file() {
335 let mut loader = SourceMapLoader::default();
338 loader.map.insert(
339 Url::parse("file:///project/dist/bundle.js").unwrap(),
340 SourceMapLoaderContent {
341 source_map: Some(ascii_str!(r#"{"version":3,"sources":["../src/index.ts"],"sourcesContent":["console.log('hello');\n"],"names":[],"mappings":"AAAA,QAAQ,IAAI"}"#).into()),
343 },
344 );
345
346 let mut source_mapper = SourceMapper::new(Rc::new(loader));
347
348 let application =
351 source_mapper.apply_source_map("file:///project/dist/bundle.js", 1, 1);
352 assert_eq!(
353 application,
354 SourceMapApplication::LineAndColumn {
355 line_number: 1,
356 column_number: 1
357 }
358 );
359 }
360
361 #[test]
362 fn test_source_map_relative_path_existing_file() {
363 let mut loader = SourceMapLoader::default();
365 loader.map.insert(
366 Url::parse("file:///project/dist/bundle.js").unwrap(),
367 SourceMapLoaderContent {
368 source_map: Some(ascii_str!(r#"{"version":3,"sources":["../src/index.ts"],"sourcesContent":["console.log('hello');\n"],"names":[],"mappings":"AAAA,QAAQ,IAAI"}"#).into()),
370 },
371 );
372 loader.add_existing_file("file:///project/src/index.ts");
373
374 let mut source_mapper = SourceMapper::new(Rc::new(loader));
375
376 let application =
377 source_mapper.apply_source_map("file:///project/dist/bundle.js", 1, 1);
378 assert_eq!(
379 application,
380 SourceMapApplication::LineAndColumnAndFileName {
381 file_name: "file:///project/src/index.ts".to_string(),
382 line_number: 1,
383 column_number: 1
384 }
385 );
386 }
387}