enhanced_magic_string/collapse_sourcemap/
mod.rs

1use std::{
2  cell::{RefCell, RefMut},
3  path::PathBuf,
4};
5
6use farmfe_utils::file_url_to_path;
7use sourcemap::{SourceMap, SourceMapBuilder, Token};
8
9pub struct CollapseSourcemapOptions {
10  /// if true, inline source content to the source map.
11  /// if the source content does not exist and source filename exists, content will be read from source file from disk.
12  pub inline_content: bool,
13
14  pub remap_source: Option<Box<dyn Fn(&str) -> String>>,
15}
16
17impl Default for CollapseSourcemapOptions {
18  fn default() -> Self {
19    Self {
20      inline_content: true,
21      remap_source: None,
22    }
23  }
24}
25
26/// collapse source map chain to a single source.
27///
28/// transformation: a -> b -> c -> d, source content is a and dest content is d.
29/// corresponding input source map: [map_a, map_b, map_c, map_d].
30///
31/// now we have d and map_d, we want to get a and map_a, we should tracing from map_d to map_a.
32///
33pub fn collapse_sourcemap_chain(
34  mut chain: Vec<SourceMap>,
35  opts: CollapseSourcemapOptions,
36) -> SourceMap {
37  chain.reverse();
38  chain = chain
39    .into_iter()
40    .filter(|map| map.get_token_count() > 0)
41    .collect();
42
43  if chain.is_empty() {
44    let builder = SourceMapBuilder::new(None);
45    return builder.into_sourcemap();
46  }
47
48  let dest_map = &chain[0];
49  let mut builder = SourceMapBuilder::new(None);
50  let mut mapped_src_cache = std::collections::HashMap::new();
51
52  // trace all tokens in cur and update
53  for token in dest_map.tokens() {
54    let mut last_map_token = token;
55    let mut completed_trace = true;
56
57    if chain.len() > 1 {
58      for map in &chain[1..] {
59        if let Some(map_token) = lookup_token(
60          map,
61          last_map_token.get_src_line(),
62          last_map_token.get_src_col(),
63        ) {
64          last_map_token = map_token;
65        } else {
66          completed_trace = false;
67          break;
68        }
69      }
70    }
71
72    // if we can't trace back to the first map, ignore this token
73    if !completed_trace {
74      // builder.add_token(&token, true);
75      continue;
76    }
77
78    let source = last_map_token.get_source();
79    let mut srd_id = None;
80
81    if let Some(src) = source {
82      let remapped_src = if let Some(remap_source) = &opts.remap_source {
83        mapped_src_cache
84          .entry(src)
85          .or_insert_with(|| remap_source(src))
86          .to_string()
87      } else {
88        src.to_string()
89      };
90
91      srd_id = Some(builder.add_source(&remapped_src));
92    }
93
94    let mut name_id = None;
95
96    if let Some(name) = last_map_token.get_name().or(token.get_name()) {
97      name_id = Some(builder.add_name(name));
98    }
99
100    let added_token = builder.add_raw(
101      token.get_dst_line(),
102      token.get_dst_col(),
103      last_map_token.get_src_line(),
104      last_map_token.get_src_col(),
105      srd_id,
106      name_id,
107      false,
108    );
109
110    if opts.inline_content && srd_id.is_some() && !builder.has_source_contents(srd_id.unwrap()) {
111      let src_content = read_source_content(last_map_token, chain.last().unwrap());
112
113      if let Some(src_content) = src_content {
114        builder.set_source_contents(added_token.src_id, Some(&src_content));
115      }
116    }
117  }
118
119  builder.into_sourcemap()
120}
121
122/// if map_token is not exact match, we should use the token next to it to make sure the line mapping is correct.
123/// this is because lookup_token of [SourceMap] will return the last found token instead of the next if it can't find exact match, which leads to wrong line mapping(mapping to previous line).
124pub fn lookup_token<'a>(map: &'a SourceMap, line: u32, col: u32) -> Option<Token<'a>> {
125  let token = map.lookup_token(line, col);
126
127  if let Some(token) = token {
128    // mapped to the last token of previous line.
129    if line > 0 && token.get_dst_line() == line - 1 && token.get_dst_col() > 0 {
130      let next_token = map.lookup_token(line + 1, 0);
131
132      if let Some(next_token) = next_token {
133        if next_token.get_dst_line() == line {
134          return Some(next_token);
135        }
136      }
137    }
138  }
139
140  token
141}
142
143pub fn read_source_content(token: Token<'_>, map: &SourceMap) -> Option<String> {
144  if let Some(view) = token.get_source_view() {
145    Some(view.source().to_string())
146  } else if let Some(src) = token.get_source() {
147    let src = &file_url_to_path(src);
148    // try read source content from disk
149    let map_file = map.get_file();
150
151    if PathBuf::from(src).is_absolute() || map_file.is_none() {
152      std::fs::read_to_string(src).ok()
153    } else if let Some(map_file) = map_file {
154      let src_file = PathBuf::from(map_file).parent().unwrap().join(src);
155      let src_content = std::fs::read_to_string(src_file).ok();
156
157      src_content
158    } else {
159      None
160    }
161  } else {
162    None
163  }
164}
165
166pub struct CollapsedSourceMap<'a> {
167  pub tokens: RefCell<Vec<Token<'a>>>,
168  pub map: SourceMap,
169}
170
171impl<'a> CollapsedSourceMap<'a> {
172  pub fn new(map: SourceMap) -> Self {
173    Self {
174      tokens: RefCell::new(vec![]),
175      map,
176    }
177  }
178
179  pub fn tokens(&'a self) -> RefMut<Vec<Token<'a>>> {
180    let mut tokens = self.tokens.borrow_mut();
181
182    if tokens.is_empty() {
183      *tokens = self.map.tokens().collect::<Vec<_>>();
184    }
185
186    tokens
187  }
188}