Library/Fn/Bundle/
builder.rs1use std::{
6 collections::{HashMap, hash_map::DefaultHasher},
7 hash::{Hash, Hasher},
8 path::Path,
9};
10
11use super::{BundleConfig, BundleEntry, BundleMode, BundleResult};
12
13pub struct BundleBuilder {
15 config:BundleConfig,
16 module_graph:HashMap<String, Vec<String>>,
18 processed:Vec<String>,
20}
21
22impl BundleBuilder {
23 pub fn new(config:BundleConfig) -> Self { Self { config, module_graph:HashMap::new(), processed:Vec::new() } }
24
25 pub fn add_entry(&mut self, entry:BundleEntry) {
27 if entry.is_entry && !self.config.entries.contains(&entry.source) {
28 self.config.entries.push(entry.source.clone());
29 }
30 }
31
32 pub fn build(&mut self) -> anyhow::Result<BundleResult> {
34 std::fs::create_dir_all(&self.config.output_dir)?;
36
37 match self.config.mode {
38 BundleMode::SingleFile => self.build_single_file(),
39 BundleMode::Bundle => self.build_bundle(),
40 BundleMode::Watch => self.build_watch(),
41 BundleMode::Esbuild => self.build_with_esbuild(),
42 }
43 }
44
45 fn build_single_file(&mut self) -> anyhow::Result<BundleResult> {
47 let mut bundled_files = Vec::new();
48
49 for entry in &self.config.entries {
50 let source_path = Path::new(entry);
51
52 if !source_path.exists() {
53 continue;
54 }
55
56 let output = self.compile_file(source_path)?;
59
60 let output_filename = source_path
62 .file_name()
63 .and_then(|n| n.to_str())
64 .unwrap_or("output.js")
65 .replace(".ts", ".js");
66
67 let output_path = Path::new(&self.config.output_dir).join(&output_filename);
68 std::fs::write(&output_path, &output)?;
69
70 bundled_files.push(output_path.to_string_lossy().to_string());
71 }
72
73 Ok(BundleResult {
74 output_path:self.config.output_dir.clone(),
75 source_map_path:None,
76 bundled_files,
77 hash:self.compute_hash(),
78 })
79 }
80
81 fn build_bundle(&mut self) -> anyhow::Result<BundleResult> {
83 let mut bundled_files = Vec::new();
84 let mut all_content = String::new();
85
86 let paths:Vec<_> = self.config.entries.iter().filter(|e| Path::new(e).exists()).cloned().collect();
88
89 for entry in paths {
91 let source_path = Path::new(&entry);
92
93 self.build_module_graph(source_path)?;
95
96 let content = self.compile_file(source_path)?;
98 all_content.push_str(&content);
99 all_content.push_str("\n");
100
101 bundled_files.push(entry);
102 }
103
104 if self.config.tree_shaking {
106 all_content = self.apply_tree_shaking(all_content);
107 }
108
109 let output_filename = self.generate_output_filename();
111 let output_path = Path::new(&self.config.output_dir).join(&output_filename);
112
113 std::fs::write(&output_path, &all_content)?;
115
116 let source_map_path = if self.config.source_map {
118 let map_path =
119 Path::new(&self.config.output_dir).join(format!("{}.map", output_filename.replace(".js", "")));
120 Some(map_path.to_string_lossy().to_string())
121 } else {
122 None
123 };
124
125 Ok(BundleResult {
126 output_path:output_path.to_string_lossy().to_string(),
127 source_map_path,
128 bundled_files,
129 hash:self.compute_hash(),
130 })
131 }
132
133 fn build_watch(&mut self) -> anyhow::Result<BundleResult> {
135 self.build_bundle()
138 }
139
140 fn build_with_esbuild(&mut self) -> anyhow::Result<BundleResult> {
142 let wrapper = super::esbuild::EsbuildWrapper::new();
144 wrapper.build(&self.config)
145 }
146
147 fn compile_file(&self, source_path:&Path) -> anyhow::Result<String> {
149 let content = std::fs::read_to_string(source_path)?;
150
151 Ok(content)
157 }
158
159 fn build_module_graph(&mut self, entry:&Path) -> anyhow::Result<()> {
161 let content = std::fs::read_to_string(entry)?;
162
163 let mut deps = Vec::new();
165 for line in content.lines() {
166 let trimmed = line.trim();
167 if trimmed.starts_with("import ") {
168 if let Some(from_idx) = trimmed.find("from") {
169 let path_part = &trimmed[from_idx + 4..];
170 if let Some(quote_start) = path_part.find('"') {
171 let path_end = path_part[quote_start + 1..].find('"');
172 if let Some(end) = path_end {
173 let import_path = &path_part[quote_start + 1..quote_start + 1 + end];
174 deps.push(import_path.to_string());
175 }
176 }
177 }
178 }
179 }
180
181 self.module_graph.insert(entry.to_string_lossy().to_string(), deps);
182
183 Ok(())
184 }
185
186 fn apply_tree_shaking(&self, content:String) -> String {
188 let mut result = String::new();
191
192 for line in content.lines() {
193 let trimmed = line.trim();
194
195 if trimmed.is_empty() || trimmed.starts_with("//") {
197 continue;
198 }
199
200 result.push_str(line);
201 result.push('\n');
202 }
203
204 result
205 }
206
207 fn generate_output_filename(&self) -> String {
209 let name = self
211 .config
212 .entries
213 .first()
214 .and_then(|e| Path::new(e).file_stem())
215 .and_then(|n| n.to_str())
216 .unwrap_or("bundle");
217
218 self.config.output_file.replace("{name}", name)
219 }
220
221 fn compute_hash(&self) -> String {
223 let mut hasher = DefaultHasher::new();
224
225 for entry in &self.config.entries {
226 entry.hash(&mut hasher);
227 }
228
229 format!("{:x}", hasher.finish())
230 }
231
232 pub fn module_graph(&self) -> &HashMap<String, Vec<String>> { &self.module_graph }
234
235 pub fn processed(&self) -> &[String] { &self.processed }
237}
238
239pub fn build_bundle(config:BundleConfig) -> anyhow::Result<BundleResult> {
241 let mut builder = BundleBuilder::new(config);
242 builder.build()
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_builder_creation() {
251 let config = BundleConfig::single_file();
252 let builder = BundleBuilder::new(config);
253
254 assert!(builder.processed.is_empty());
255 }
256
257 #[test]
258 fn test_add_entry() {
259 let config = BundleConfig::bundle();
260 let mut builder = BundleBuilder::new(config);
261
262 builder.add_entry(BundleEntry::entry("src/index.ts"));
263
264 assert_eq!(builder.config.entries.len(), 1);
265 }
266
267 #[test]
268 fn test_output_filename() {
269 let config = BundleConfig::bundle().with_output_file("{name}.bundle.js");
270
271 let builder = BundleBuilder::new(config);
272 let filename = builder.generate_output_filename();
273
274 assert_eq!(filename, "index.bundle.js");
275 }
276
277 #[test]
278 fn test_hash_computation() {
279 let config = BundleConfig::bundle().add_entry("src/index.ts").add_entry("src/util.ts");
280
281 let builder = BundleBuilder::new(config);
282 let hash = builder.compute_hash();
283
284 assert!(!hash.is_empty());
285 }
286}