enhanced_magic_string/
bundle.rs

1use std::collections::HashMap;
2
3use sourcemap::{SourceMap, SourceMapBuilder};
4
5use crate::{
6  collapse_sourcemap::{lookup_token, read_source_content},
7  error::{Error, Result},
8  magic_string::MagicString,
9  mappings::Mappings,
10  types::SourceMapOptions,
11  utils::{char_string::CharString, common::get_relative_path, get_locator::get_locator},
12};
13
14#[derive(Default)]
15pub struct BundleOptions {
16  pub separator: Option<char>,
17  pub intro: Option<CharString>,
18  pub trace_source_map_chain: Option<bool>,
19}
20
21struct UniqueSource {
22  pub filename: String,
23  pub content: CharString,
24}
25
26pub struct AddSourceOptions {
27  pub separator: char,
28  pub filename: Option<String>,
29}
30
31pub struct Bundle {
32  separator: char,
33  intro: CharString,
34  sources: Vec<MagicString>,
35  unique_sources: Vec<UniqueSource>,
36  unique_source_index_by_filename: HashMap<String, usize>,
37  trace_source_map_chain: bool,
38}
39
40impl Bundle {
41  pub fn new(options: BundleOptions) -> Self {
42    Self {
43      separator: options.separator.unwrap_or('\n'),
44      intro: options.intro.unwrap_or("".into()),
45      sources: vec![],
46      unique_sources: vec![],
47      unique_source_index_by_filename: HashMap::new(),
48      trace_source_map_chain: options.trace_source_map_chain.unwrap_or(false),
49    }
50  }
51
52  pub fn add_source(
53    &mut self,
54    mut source: MagicString,
55    opts: Option<AddSourceOptions>,
56  ) -> Result<()> {
57    let filename = opts
58      .as_ref()
59      .and_then(|opts| opts.filename.as_ref())
60      .or(source.filename.as_ref());
61    let separator = opts
62      .as_ref()
63      .map(|opts| opts.separator)
64      .unwrap_or(self.separator);
65    source.separator = separator;
66
67    if let Some(filename) = filename {
68      if let Some(index) = self.unique_source_index_by_filename.get(filename) {
69        let unique_source = &self.unique_sources[*index];
70
71        if unique_source.content != source.original {
72          return Err(Error::IllegalSource);
73        }
74      } else {
75        self
76          .unique_source_index_by_filename
77          .insert(filename.clone(), self.unique_sources.len());
78        self.unique_sources.push(UniqueSource {
79          filename: filename.clone(),
80          content: source.original.clone(),
81        });
82      }
83    }
84
85    self.sources.push(source);
86
87    Ok(())
88  }
89
90  pub fn generate_map(&self, opts: SourceMapOptions) -> Result<SourceMap> {
91    let mut names = vec![];
92    // let mut x_google_ignoreList = None;
93
94    self.sources.iter().for_each(|source| {
95      source.stored_names.iter().for_each(|(name, _)| {
96        names.push(name.clone());
97      });
98    });
99
100    let mut mappings = Mappings::new(opts.hires.unwrap_or_default());
101
102    if !self.intro.is_empty() {
103      mappings.advance(&self.intro);
104    }
105
106    self.sources.iter().enumerate().for_each(|(i, source)| {
107      if i > 0 {
108        // replace \0 to empty string
109        let separator = if source.separator == '\0' {
110          CharString::new("")
111        } else {
112          CharString::from(source.separator)
113        };
114        mappings.advance(&separator);
115      }
116
117      let source_index: isize = if let Some(filename) = &source.filename {
118        (*self.unique_source_index_by_filename.get(filename).unwrap())
119          .try_into()
120          .unwrap()
121      } else {
122        -1
123      };
124      let locate = get_locator(&source.original);
125
126      if !source.intro.is_empty() {
127        mappings.advance(&source.intro);
128      }
129
130      source.first_chunk.lock().each_next(|chunk| {
131        let loc = locate(chunk.start);
132
133        if !chunk.intro.is_empty() {
134          mappings.advance(&chunk.intro);
135        }
136
137        if source.filename.is_some() {
138          if chunk.edited {
139            unimplemented!("chunk.edited");
140          } else {
141            mappings.add_unedited_chunk(
142              source_index,
143              chunk,
144              &source.original,
145              loc,
146              &source.sourcemap_locations,
147            );
148          }
149        } else {
150          mappings.advance(&chunk.content);
151        }
152
153        if !chunk.outro.is_empty() {
154          mappings.advance(&chunk.outro);
155        }
156      });
157
158      if !source.outro.is_empty() {
159        mappings.advance(&source.outro);
160      }
161
162      if !source.ignore_list.is_empty() {
163        unimplemented!("source.ignore_list");
164      }
165    });
166
167    let mut sourcemap_builder = SourceMapBuilder::new(opts.file.as_ref().map(|f| f.as_str()));
168
169    self.unique_sources.iter().for_each(|source| {
170      let filename = if let Some(file) = &opts.file {
171        get_relative_path(file, &source.filename).unwrap()
172      } else {
173        source.filename.clone()
174      };
175      let src_id = sourcemap_builder.add_source(&filename);
176      let inline_content = opts.include_content.unwrap_or(false);
177      let content = if inline_content {
178        Some(source.content.to_string())
179      } else {
180        None
181      };
182      sourcemap_builder.set_source_contents(src_id, content.as_deref());
183    });
184
185    names.into_iter().for_each(|name| {
186      sourcemap_builder.add_name(&name.to_string());
187    });
188
189    mappings.into_sourcemap_mappings(&mut sourcemap_builder);
190
191    if self.trace_source_map_chain {
192      let map = sourcemap_builder.into_sourcemap();
193      // try trace back to original sourcemap of each source
194      let mut trace_sourcemap_builder =
195        SourceMapBuilder::new(opts.file.as_ref().map(|f| f.as_str()));
196      let mut collapsed_sourcemap_cache = HashMap::new();
197      let mut mapped_src_cache = HashMap::new();
198
199      for token in map.tokens() {
200        if let Some(source_filename) = token.get_source() {
201          if let Some(source) = self.get_source_by_filename(source_filename) {
202            let source_map_chain = collapsed_sourcemap_cache
203              .entry(source_filename.to_string())
204              .or_insert_with(|| source.get_source_map_chain());
205
206            let mut is_trace_completed = true;
207            let mut map_token = token;
208            // let mut traced_tokens = vec![token];
209
210            for map in source_map_chain.iter() {
211              // if the token can not be found in source map chain, it will be ignored.
212              if let Some(m_token) =
213                lookup_token(map, map_token.get_src_line(), map_token.get_src_col())
214              {
215                map_token = m_token;
216                // traced_tokens.push(map_token);
217              } else {
218                is_trace_completed = false;
219                break;
220              }
221            }
222
223            if is_trace_completed {
224              // // print the traced token chain
225              // for token in traced_tokens.iter() {
226              //   print!(
227              //     "({}:{}:{}:{}) -> ",
228              //     token.get_dst_line(),
229              //     token.get_dst_col(),
230              //     token.get_src_line(),
231              //     token.get_src_col()
232              //   );
233              // }
234              // println!();
235
236              let src = if let Some(src) = map_token.get_source() {
237                Some(if let Some(remap_source) = &opts.remap_source {
238                  mapped_src_cache
239                    .entry(src.to_string())
240                    .or_insert_with(|| remap_source(src))
241                    .to_string()
242                } else {
243                  src.to_string()
244                })
245              } else {
246                None
247              };
248
249              let added_token = trace_sourcemap_builder.add(
250                token.get_dst_line(),
251                token.get_dst_col(),
252                map_token.get_src_line(),
253                map_token.get_src_col(),
254                src.as_deref(),
255                map_token.get_name(),
256                false,
257              );
258
259              let inline_content = opts.include_content.unwrap_or(false);
260
261              if inline_content && !trace_sourcemap_builder.has_source_contents(added_token.src_id)
262              {
263                let source_content =
264                  read_source_content(map_token, source_map_chain.last().unwrap_or(&map));
265
266                if let Some(source_content) = source_content {
267                  trace_sourcemap_builder
268                    .set_source_contents(added_token.src_id, Some(&source_content));
269                }
270              }
271            }
272          }
273        }
274      }
275
276      return Ok(trace_sourcemap_builder.into_sourcemap());
277    }
278
279    Ok(sourcemap_builder.into_sourcemap())
280  }
281
282  fn get_source_by_filename(&self, filename: &str) -> Option<&MagicString> {
283    let source_index = self.unique_source_index_by_filename.get(filename)?;
284
285    self.sources.get(*source_index)
286  }
287
288  pub fn append(&mut self, str: &str, opts: Option<AddSourceOptions>) {
289    self
290      .add_source(
291        MagicString::new(str, None),
292        opts.or(Some(AddSourceOptions {
293          separator: '\0',
294          filename: None,
295        })),
296      )
297      .unwrap();
298  }
299
300  pub fn prepend(&mut self, str: &str) {
301    let mut new_intro = CharString::new(str);
302    new_intro.append(&self.intro);
303    self.intro = new_intro;
304  }
305}
306
307impl ToString for Bundle {
308  fn to_string(&self) -> String {
309    let body = self
310      .sources
311      .iter()
312      .enumerate()
313      .map(|(i, source)| {
314        let separator = if i > 0 && source.separator != '\0' {
315          source.separator.to_string()
316        } else {
317          "".to_string()
318        };
319
320        format!("{}{}", separator, source.to_string())
321      })
322      .collect::<Vec<_>>()
323      .join("");
324
325    format!("{}{}", self.intro, body)
326  }
327}