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
29 self
30 }
31
32 fn find_esbuild(&self) -> Option<String> {
34 if let Some(ref path) = self.esbuild_path {
36 if Path::new(path).exists() {
37 return Some(path.clone());
38 }
39 }
40
41 let possible_paths = [
43 "./node_modules/.bin/esbuild",
44 "./node_modules/esbuild/bin/esbuild",
45 "../node_modules/.bin/esbuild",
46 "../../node_modules/.bin/esbuild",
47 ];
48
49 for path in &possible_paths {
50 if Path::new(path).exists() {
51 return Some(path.to_string());
52 }
53 }
54
55 if Command::new("esbuild").arg("--version").output().is_ok() {
57 return Some("esbuild".to_string());
58 }
59
60 None
61 }
62
63 pub fn is_available(&self) -> bool { self.find_esbuild().is_some() }
65
66 pub fn build(&self, config:&BundleConfig) -> anyhow::Result<BundleResult> {
68 let esbuild_path = self
69 .find_esbuild()
70 .ok_or_else(|| anyhow::anyhow!("esbuild not found. Please install it with: npm install esbuild"))?;
71
72 let mut args = Vec::new();
74
75 for entry in &config.entries {
77 args.push("--bundle".to_string());
78
79 args.push(entry.clone());
80 }
81
82 args.push("--outdir".to_string());
84
85 args.push(config.output_dir.clone());
86
87 args.push("--format".to_string());
89
90 args.push(config.format.clone());
91
92 args.push("--target".to_string());
94
95 args.push(config.target.clone());
96
97 if config.source_map {
99 args.push("--sourcemap".to_string());
100 }
101
102 if config.inline_source_map {
103 args.push("--sourcemap=inline".to_string());
104 }
105
106 if config.minify {
108 args.push("--minify".to_string());
109 }
110
111 if !config.tree_shaking {
113 args.push("--tree-shaking=false".to_string());
114 }
115
116 if config.watch {
118 args.push("--watch".to_string());
119 }
120
121 for external in &config.externals {
123 args.push("--external".to_string());
124
125 args.push(external.clone());
126 }
127
128 args.push("--platform=browser".to_string());
130
131 let output = Command::new(&esbuild_path)
133 .args(&args)
134 .output()
135 .map_err(|e| anyhow::anyhow!("Failed to run esbuild: {}", e))?;
136
137 if !output.status.success() {
138 let stderr = String::from_utf8_lossy(&output.stderr);
139
140 return Err(anyhow::anyhow!("esbuild failed: {}", stderr));
141 }
142
143 let mut bundled_files = Vec::new();
145
146 for entry in &config.entries {
147 let filename = Path::new(entry).file_stem().and_then(|s| s.to_str()).unwrap_or("output");
148
149 let ext = if config.format == "cjs" { "cjs" } else { "js" };
150
151 bundled_files.push(format!("{}/{}.{}", config.output_dir, filename, ext));
152 }
153
154 Ok(BundleResult {
155 output_path:config.output_dir.clone(),
156 source_map_path:if config.source_map {
157 Some(format!("{}.map", config.output_dir))
158 } else {
159 None
160 },
161 bundled_files,
162 hash:format!(
163 "{:x}",
164 std::time::SystemTime::now()
165 .duration_since(std::time::UNIX_EPOCH)
166 .unwrap()
167 .as_millis()
168 ),
169 })
170 }
171
172 pub fn build_with_types(&self, config:&BundleConfig) -> anyhow::Result<BundleResult> {
174 let mut args = vec!["--bundle".to_string(), "--loader:.ts=ts".to_string()];
175
176 for entry in &config.entries {
178 args.push(entry.clone());
179 }
180
181 args.push("--outdir".to_string());
182
183 args.push(config.output_dir.clone());
184
185 args.push("--tsconfig".to_string());
187
188 args.push("tsconfig.json".to_string());
189
190 self.build(config)
191 }
192
193 pub fn watch<F>(&self, config:&BundleConfig, _on_change:F) -> anyhow::Result<()>
195 where
196 F: Fn(&str) + Send + Sync, {
197 let _esbuild_path = self.find_esbuild().ok_or_else(|| anyhow::anyhow!("esbuild not found"))?;
198
199 self.build(config)?;
201
202 tracing::info!("Watch mode enabled. Rebuilding on file changes...");
205
206 Ok(())
207 }
208}
209
210impl Default for EsbuildWrapper {
211 fn default() -> Self { Self::new() }
212}
213
214pub fn check_esbuild() -> bool {
216 let wrapper = EsbuildWrapper::new();
217
218 wrapper.is_available()
219}
220
221pub fn install_esbuild() -> anyhow::Result<()> {
223 let output = Command::new("npm")
224 .args(&["install", "esbuild"])
225 .output()
226 .map_err(|e| anyhow::anyhow!("Failed to run npm: {}", e))?;
227
228 if !output.status.success() {
229 return Err(anyhow::anyhow!("Failed to install esbuild"));
230 }
231
232 Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237
238 use super::*;
239
240 #[test]
241 fn test_esbuild_wrapper_creation() {
242 let wrapper = EsbuildWrapper::new();
243
244 assert!(wrapper.esbuild_path.is_none());
245 }
246
247 #[test]
248 fn test_esbuild_wrapper_with_path() {
249 let wrapper = EsbuildWrapper::new().with_path("/custom/path/esbuild");
250
251 assert!(wrapper.esbuild_path.is_some());
252 }
253
254 #[test]
255 fn test_check_esbuild() {
256 let available = check_esbuild();
258
259 let _ = available;
261 }
262}