Library/Fn/Bundle/
esbuild.rs1use std::{path::Path, process::Command};
14
15use super::{BundleConfig, BundleResult};
16
17pub struct EsbuildWrapper {
19 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 fn find_esbuild(&self) -> Option<String> {
33 if let Some(ref path) = self.esbuild_path {
35 if Path::new(path).exists() {
36 return Some(path.clone());
37 }
38 }
39
40 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 if Command::new("esbuild").arg("--version").output().is_ok() {
56 return Some("esbuild".to_string());
57 }
58
59 None
60 }
61
62 pub fn is_available(&self) -> bool { self.find_esbuild().is_some() }
64
65 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 let mut args = Vec::new();
73
74 for entry in &config.entries {
76 args.push("--bundle".to_string());
77 args.push(entry.clone());
78 }
79
80 args.push("--outdir".to_string());
82 args.push(config.output_dir.clone());
83
84 args.push("--format".to_string());
86 args.push(config.format.clone());
87
88 args.push("--target".to_string());
90 args.push(config.target.clone());
91
92 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 if config.minify {
103 args.push("--minify".to_string());
104 }
105
106 if !config.tree_shaking {
108 args.push("--tree-shaking=false".to_string());
109 }
110
111 if config.watch {
113 args.push("--watch".to_string());
114 }
115
116 for external in &config.externals {
118 args.push("--external".to_string());
119 args.push(external.clone());
120 }
121
122 args.push("--platform=browser".to_string());
124
125 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 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 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 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 args.push("--tsconfig".to_string());
177 args.push("tsconfig.json".to_string());
178
179 self.build(config)
180 }
181
182 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 self.build(config)?;
190
191 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
203pub fn check_esbuild() -> bool {
205 let wrapper = EsbuildWrapper::new();
206 wrapper.is_available()
207}
208
209pub 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 let available = check_esbuild();
243 let _ = available;
245 }
246}