enhanced_magic_string/
bundle.rs1use 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 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 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 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 for map in source_map_chain.iter() {
211 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 } else {
218 is_trace_completed = false;
219 break;
220 }
221 }
222
223 if is_trace_completed {
224 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}