Skip to main content

Library/Fn/Bundle/
esbuild.rs

1//! esbuild wrapper for complex builds
2//!
3//! This module provides an optional wrapper that invokes esbuild for builds
4//! that Rest can't handle alone. It's designed as a fallback for complex
5//! scenarios:
6//! - Large multi-file bundles
7//! - Complex module resolution
8//! - Advanced tree-shaking
9//! - AMD module output
10//!
11//! Note: This requires esbuild to be installed separately.
12
13use std::{path::Path, process::Command};
14
15use super::{BundleConfig, BundleResult};
16
17/// Wrapper around esbuild for complex builds
18pub struct EsbuildWrapper {
19	/// Path to esbuild binary (defaults to node_modules/.bin/esbuild)
20	esbuild_path:Option<String>,
21}
22
23impl EsbuildWrapper {
24	pub fn new() -> Self { Self { esbuild_path:None } }
25
26	pub fn with_path(mut self, path:impl Into<String>) -> Self {
27		self.esbuild_path = Some(path.into());
28		self
29	}
30
31	/// Find esbuild in common locations
32	fn find_esbuild(&self) -> Option<String> {
33		// Check explicit path first
34		if let Some(ref path) = self.esbuild_path {
35			if Path::new(path).exists() {
36				return Some(path.clone());
37			}
38		}
39
40		// Check node_modules
41		let possible_paths = [
42			"./node_modules/.bin/esbuild",
43			"./node_modules/esbuild/bin/esbuild",
44			"../node_modules/.bin/esbuild",
45			"../../node_modules/.bin/esbuild",
46		];
47
48		for path in &possible_paths {
49			if Path::new(path).exists() {
50				return Some(path.to_string());
51			}
52		}
53
54		// Check if esbuild is in PATH
55		if Command::new("esbuild").arg("--version").output().is_ok() {
56			return Some("esbuild".to_string());
57		}
58
59		None
60	}
61
62	/// Check if esbuild is available
63	pub fn is_available(&self) -> bool { self.find_esbuild().is_some() }
64
65	/// Build using esbuild
66	pub fn build(&self, config:&BundleConfig) -> anyhow::Result<BundleResult> {
67		let esbuild_path = self
68			.find_esbuild()
69			.ok_or_else(|| anyhow::anyhow!("esbuild not found. Please install it with: npm install esbuild"))?;
70
71		// Build esbuild arguments
72		let mut args = Vec::new();
73
74		// Entry points
75		for entry in &config.entries {
76			args.push("--bundle".to_string());
77			args.push(entry.clone());
78		}
79
80		// Output
81		args.push("--outdir".to_string());
82		args.push(config.output_dir.clone());
83
84		// Format
85		args.push("--format".to_string());
86		args.push(config.format.clone());
87
88		// Target
89		args.push("--target".to_string());
90		args.push(config.target.clone());
91
92		// Source maps
93		if config.source_map {
94			args.push("--sourcemap".to_string());
95		}
96
97		if config.inline_source_map {
98			args.push("--sourcemap=inline".to_string());
99		}
100
101		// Minification
102		if config.minify {
103			args.push("--minify".to_string());
104		}
105
106		// Tree-shaking (esbuild enables this by default)
107		if !config.tree_shaking {
108			args.push("--tree-shaking=false".to_string());
109		}
110
111		// Watch mode
112		if config.watch {
113			args.push("--watch".to_string());
114		}
115
116		// External modules
117		for external in &config.externals {
118			args.push("--external".to_string());
119			args.push(external.clone());
120		}
121
122		// Platform
123		args.push("--platform=browser".to_string());
124
125		// Execute esbuild
126		let output = Command::new(&esbuild_path)
127			.args(&args)
128			.output()
129			.map_err(|e| anyhow::anyhow!("Failed to run esbuild: {}", e))?;
130
131		if !output.status.success() {
132			let stderr = String::from_utf8_lossy(&output.stderr);
133			return Err(anyhow::anyhow!("esbuild failed: {}", stderr));
134		}
135
136		// Collect bundled files
137		let mut bundled_files = Vec::new();
138		for entry in &config.entries {
139			let filename = Path::new(entry).file_stem().and_then(|s| s.to_str()).unwrap_or("output");
140
141			let ext = if config.format == "cjs" { "cjs" } else { "js" };
142			bundled_files.push(format!("{}/{}.{}", config.output_dir, filename, ext));
143		}
144
145		Ok(BundleResult {
146			output_path:config.output_dir.clone(),
147			source_map_path:if config.source_map {
148				Some(format!("{}.map", config.output_dir))
149			} else {
150				None
151			},
152			bundled_files,
153			hash:format!(
154				"{:x}",
155				std::time::SystemTime::now()
156					.duration_since(std::time::UNIX_EPOCH)
157					.unwrap()
158					.as_millis()
159			),
160		})
161	}
162
163	/// Build with TypeScript type checking
164	pub fn build_with_types(&self, config:&BundleConfig) -> anyhow::Result<BundleResult> {
165		let mut args = vec!["--bundle".to_string(), "--loader:.ts=ts".to_string()];
166
167		// Add all config args
168		for entry in &config.entries {
169			args.push(entry.clone());
170		}
171
172		args.push("--outdir".to_string());
173		args.push(config.output_dir.clone());
174
175		// Type checking
176		args.push("--tsconfig".to_string());
177		args.push("tsconfig.json".to_string());
178
179		self.build(config)
180	}
181
182	/// Watch mode with callback
183	pub fn watch<F>(&self, config:&BundleConfig, _on_change:F) -> anyhow::Result<()>
184	where
185		F: Fn(&str) + Send + Sync, {
186		let _esbuild_path = self.find_esbuild().ok_or_else(|| anyhow::anyhow!("esbuild not found"))?;
187
188		// Build initial bundle
189		self.build(config)?;
190
191		// Set up file watcher (simplified - would use notify crate in production)
192		// For now, just build once
193		tracing::info!("Watch mode enabled. Rebuilding on file changes...");
194
195		Ok(())
196	}
197}
198
199impl Default for EsbuildWrapper {
200	fn default() -> Self { Self::new() }
201}
202
203/// Check if esbuild is available
204pub fn check_esbuild() -> bool {
205	let wrapper = EsbuildWrapper::new();
206	wrapper.is_available()
207}
208
209/// Install esbuild if not present
210pub fn install_esbuild() -> anyhow::Result<()> {
211	let output = Command::new("npm")
212		.args(&["install", "esbuild"])
213		.output()
214		.map_err(|e| anyhow::anyhow!("Failed to run npm: {}", e))?;
215
216	if !output.status.success() {
217		return Err(anyhow::anyhow!("Failed to install esbuild"));
218	}
219
220	Ok(())
221}
222
223#[cfg(test)]
224mod tests {
225	use super::*;
226
227	#[test]
228	fn test_esbuild_wrapper_creation() {
229		let wrapper = EsbuildWrapper::new();
230		assert!(wrapper.esbuild_path.is_none());
231	}
232
233	#[test]
234	fn test_esbuild_wrapper_with_path() {
235		let wrapper = EsbuildWrapper::new().with_path("/custom/path/esbuild");
236		assert!(wrapper.esbuild_path.is_some());
237	}
238
239	#[test]
240	fn test_check_esbuild() {
241		// This test will fail if esbuild is not installed
242		let available = check_esbuild();
243		// Just check it doesn't panic
244		let _ = available;
245	}
246}