Skip to main content

Library/Fn/Bundle/
builder.rs

1//! Bundle builder
2//!
3//! Orchestrates the bundling process using Rest compiler + optional esbuild.
4
5use std::{
6	collections::{HashMap, hash_map::DefaultHasher},
7	hash::{Hash, Hasher},
8	path::Path,
9};
10
11use super::{BundleConfig, BundleEntry, BundleMode, BundleResult};
12
13/// Builds bundles from input files
14pub struct BundleBuilder {
15	config:BundleConfig,
16	/// Cached module graph
17	module_graph:HashMap<String, Vec<String>>,
18	/// Processed files
19	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	/// Add an entry point to the bundle
26	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	/// Build the bundle
33	pub fn build(&mut self) -> anyhow::Result<BundleResult> {
34		// Ensure output directory exists
35		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	/// Build in single-file mode (current behavior)
46	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			// Compile using Rest compiler (simulated here - actual implementation
57			// would use the Compiler from Struct/SWC.rs)
58			let output = self.compile_file(source_path)?;
59
60			// Write output
61			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	/// Build a bundle from multiple files
82	fn build_bundle(&mut self) -> anyhow::Result<BundleResult> {
83		let mut bundled_files = Vec::new();
84		let mut all_content = String::new();
85
86		// Collect paths first to avoid borrow issues
87		let paths:Vec<_> = self.config.entries.iter().filter(|e| Path::new(e).exists()).cloned().collect();
88
89		// Process each entry
90		for entry in paths {
91			let source_path = Path::new(&entry);
92
93			// Build module graph
94			self.build_module_graph(source_path)?;
95
96			// Compile and collect content
97			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		// Apply tree-shaking if enabled
105		if self.config.tree_shaking {
106			all_content = self.apply_tree_shaking(all_content);
107		}
108
109		// Generate output filename
110		let output_filename = self.generate_output_filename();
111		let output_path = Path::new(&self.config.output_dir).join(&output_filename);
112
113		// Write bundle
114		std::fs::write(&output_path, &all_content)?;
115
116		// Generate source map if enabled
117		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	/// Build in watch mode
134	fn build_watch(&mut self) -> anyhow::Result<BundleResult> {
135		// For watch mode, build once and set up file watching
136		// The actual watching would be handled by the caller
137		self.build_bundle()
138	}
139
140	/// Build using esbuild wrapper
141	fn build_with_esbuild(&mut self) -> anyhow::Result<BundleResult> {
142		// Import and use the esbuild wrapper
143		let wrapper = super::esbuild::EsbuildWrapper::new();
144		wrapper.build(&self.config)
145	}
146
147	/// Compile a single file using Rest
148	fn compile_file(&self, source_path:&Path) -> anyhow::Result<String> {
149		let content = std::fs::read_to_string(source_path)?;
150
151		// In a full implementation, this would:
152		// 1. Parse with SWC
153		// 2. Apply transforms
154		// 3. Generate output
155		// For now, return a placeholder
156		Ok(content)
157	}
158
159	/// Build module graph for dependencies
160	fn build_module_graph(&mut self, entry:&Path) -> anyhow::Result<()> {
161		let content = std::fs::read_to_string(entry)?;
162
163		// Extract imports (simplified)
164		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	/// Apply tree-shaking to bundle content
187	fn apply_tree_shaking(&self, content:String) -> String {
188		// Simplified tree-shaking: remove comments and whitespace
189		// A full implementation would analyze the AST for used exports
190		let mut result = String::new();
191
192		for line in content.lines() {
193			let trimmed = line.trim();
194
195			// Skip empty lines and single-line comments
196			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	/// Generate output filename from config
208	fn generate_output_filename(&self) -> String {
209		// Use first entry name or default
210		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	/// Compute hash of bundle for cache invalidation
222	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	/// Get the module graph
233	pub fn module_graph(&self) -> &HashMap<String, Vec<String>> { &self.module_graph }
234
235	/// Get processed files
236	pub fn processed(&self) -> &[String] { &self.processed }
237}
238
239/// Convenience function to create and build a bundle
240pub 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}