1use crate::cli::DevArgs;
11use crate::dev::{
12 DevBuilder, DevConfig, DevEvent, DevServer, DevServerState, FileChange, FileWatcher,
13 SharedState,
14};
15use crate::error::Result;
16use crate::ui;
17use std::sync::Arc;
18use tokio::signal;
19
20pub async fn execute(args: DevArgs) -> Result<()> {
46 ui::info("Starting development server...");
47
48 let config = DevConfig::from_args(&args)?;
50 config.validate()?;
51
52 let entry_display = if let Some(ref entry) = args.entry {
53 entry.display().to_string()
54 } else {
55 config
56 .base
57 .entry
58 .first()
59 .cloned()
60 .unwrap_or_else(|| "unknown".to_string())
61 };
62 ui::info(&format!("Entry point: {}", entry_display));
63 ui::info(&format!("Working directory: {}", config.cwd.display()));
64
65 let out_dir = if config.base.out_dir.is_absolute() {
68 config.base.out_dir.clone()
69 } else {
70 config.cwd.join(&config.base.out_dir)
71 };
72 let state = Arc::new(DevServerState::new(out_dir));
73
74 let builder = DevBuilder::new(config.base.clone(), config.cwd.clone());
76
77 ui::info("Performing initial build...");
79 state.start_build();
80
81 match builder.initial_build().await {
82 Ok((duration_ms, cache, asset_registry)) => {
83 state.complete_build(duration_ms);
84 ui::success(&format!("Initial build completed in {}ms", duration_ms));
85
86 state.update_cache(cache);
88 if let Some(registry) = asset_registry {
89 state.update_asset_registry(registry);
90 }
91
92 ui::info(&format!(
93 "Cached {} files in memory",
94 state.cache.read().len()
95 ));
96 }
97 Err(e) => {
98 let error_msg = e.to_string();
99 state.fail_build(error_msg.clone());
100 ui::error(&format!("Initial build failed: {}", error_msg));
101 return Err(e);
102 }
103 }
104
105 let (watcher, mut change_rx) = FileWatcher::new(
107 config.cwd.clone(),
108 config.watch_ignore.clone(),
109 config.debounce_ms,
110 )?;
111
112 ui::info(&format!(
113 "Watching for changes in: {}",
114 watcher.root().display()
115 ));
116
117 let server = DevServer::new(config.clone(), state.clone());
119 let mut server_handle = tokio::spawn(async move {
120 if let Err(e) = server.start().await {
121 ui::error(&format!("Server error: {}", e));
122 }
123 });
124
125 if config.open {
127 open_browser(&config.server_url());
128 }
129
130 ui::info("Press Ctrl+C to stop");
132
133 loop {
134 tokio::select! {
135 Some(change) = change_rx.recv() => {
137 handle_file_change(change, &builder, &state).await;
138 }
139
140 _ = signal::ctrl_c() => {
142 ui::info("Shutting down development server...");
143 break;
144 }
145
146 _ = &mut server_handle => {
148 ui::warning("Server task completed unexpectedly");
149 break;
150 }
151 }
152 }
153
154 ui::success("Development server stopped");
155 Ok(())
156}
157
158async fn handle_file_change(change: FileChange, builder: &DevBuilder, state: &SharedState) {
162 let path = change.path();
163 ui::info(&format!("File changed: {}", path.display()));
164
165 fob_bundler::diagnostics::clear_source_cache();
167
168 state.start_build();
170 let _ = state.broadcast(&DevEvent::BuildStarted).await;
171
172 match builder.rebuild().await {
174 Ok((duration_ms, cache, asset_registry)) => {
175 state.complete_build(duration_ms);
177 state.update_cache(cache);
178 if let Some(registry) = asset_registry {
179 state.update_asset_registry(registry);
180 }
181
182 ui::success(&format!("Rebuild completed in {}ms", duration_ms));
183
184 let _ = state
186 .broadcast(&DevEvent::BuildCompleted { duration_ms })
187 .await;
188 }
189 Err(e) => {
190 let error_msg = e.to_string();
191 state.fail_build(error_msg.clone());
192
193 ui::error(&format!("Rebuild failed: {}", error_msg));
194
195 let _ = state
197 .broadcast(&DevEvent::BuildFailed { error: error_msg })
198 .await;
199 }
200 }
201}
202
203fn open_browser(url: &str) {
210 use std::process::Command;
211
212 let result = if cfg!(target_os = "macos") {
213 Command::new("open").arg(url).spawn()
214 } else if cfg!(target_os = "windows") {
215 Command::new("cmd").args(["/C", "start", url]).spawn()
216 } else {
217 Command::new("xdg-open").arg(url).spawn()
218 };
219
220 match result {
221 Ok(_) => ui::info(&format!("Opened browser at {}", url)),
222 Err(e) => ui::warning(&format!("Failed to open browser: {}", e)),
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 #[test]
229 fn test_open_browser_url_format() {
230 let urls = vec![
233 "http://localhost:3000",
234 "http://127.0.0.1:3000",
235 "https://localhost:3000",
236 ];
237
238 for url in urls {
239 let _ = std::panic::catch_unwind(|| {
241 assert!(url.starts_with("http"));
244 });
245 }
246 }
247}