1use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10use tokio::sync::broadcast;
11
12#[derive(Debug, thiserror::Error)]
14pub enum DevServerError {
15 #[error("Server startup failed: {0}")]
16 StartupFailed(String),
17
18 #[error("File watcher error: {0}")]
19 FileWatcherError(String),
20
21 #[error("WebSocket error: {0}")]
22 WebSocketError(String),
23
24 #[error("Build error: {0}")]
25 BuildError(String),
26
27 #[error("Port already in use: {0}")]
28 PortInUse(u16),
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct FileChangeEvent {
34 pub file_path: String,
35 pub change_type: FileChangeType,
36 pub timestamp: u64,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum FileChangeType {
41 Created,
42 Modified,
43 Deleted,
44 Renamed { from: String, to: String },
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct HotReloadMessage {
50 pub message_type: HotReloadMessageType,
51 pub payload: serde_json::Value,
52 pub timestamp: u64,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub enum HotReloadMessageType {
57 FileChanged,
58 BuildComplete,
59 BuildError,
60 FullReload,
61 CssUpdate,
62 JsUpdate,
63}
64
65#[derive(Debug, Clone)]
67pub struct DevServerConfig {
68 pub port: u16,
69 pub host: String,
70 pub project_root: PathBuf,
71 pub watch_paths: Vec<PathBuf>,
72 pub ignore_patterns: Vec<String>,
73 pub build_command: Option<String>,
74 pub hot_reload_enabled: bool,
75 pub websocket_enabled: bool,
76 pub debounce_ms: u64,
77}
78
79impl Default for DevServerConfig {
80 fn default() -> Self {
81 Self {
82 port: 3000,
83 host: "localhost".to_string(),
84 project_root: PathBuf::from("."),
85 watch_paths: vec![
86 PathBuf::from("src"),
87 PathBuf::from("examples"),
88 PathBuf::from("assets"),
89 ],
90 ignore_patterns: vec![
91 ".git".to_string(),
92 "target".to_string(),
93 "node_modules".to_string(),
94 "*.tmp".to_string(),
95 ],
96 build_command: Some("cargo build".to_string()),
97 hot_reload_enabled: true,
98 websocket_enabled: true,
99 debounce_ms: 300,
100 }
101 }
102}
103
104pub struct DevServer {
106 config: DevServerConfig,
107 file_watcher: Option<FileWatcher>,
108 websocket_server: Option<WebSocketServer>,
109 build_manager: BuildManager,
110 connected_clients: Arc<Mutex<Vec<WebSocketClient>>>,
111 change_sender: broadcast::Sender<FileChangeEvent>,
112 running: bool,
113}
114
115impl DevServer {
116 pub fn new<P: AsRef<Path>>(project_root: P, port: u16) -> Self {
118 let mut config = DevServerConfig::default();
119 config.project_root = project_root.as_ref().to_path_buf();
120 config.port = port;
121
122 let (change_sender, _) = broadcast::channel(100);
123
124 Self {
125 config,
126 file_watcher: None,
127 websocket_server: None,
128 build_manager: BuildManager::new(),
129 connected_clients: Arc::new(Mutex::new(Vec::new())),
130 change_sender,
131 running: false,
132 }
133 }
134
135 pub async fn start(&mut self) -> Result<(), DevServerError> {
137 if self.running {
138 return Ok(());
139 }
140
141 self.start_file_watcher().await?;
143
144 self.build_manager.start().await?;
146
147 self.start_http_server().await?;
149
150 self.running = true;
151
152 println!(
153 "🚀 Dev server started on http://{}:{}",
154 self.config.host, self.config.port
155 );
156 println!("📁 Watching: {:?}", self.config.watch_paths);
157
158 Ok(())
159 }
160
161 pub async fn start_with_websockets(&mut self) -> Result<(), DevServerError> {
163 self.start().await?;
164
165 if self.config.websocket_enabled {
166 self.start_websocket_server().await?;
167 }
168
169 Ok(())
170 }
171
172 pub fn stop(&mut self) {
174 if !self.running {
175 return;
176 }
177
178 if let Some(watcher) = &mut self.file_watcher {
179 watcher.stop();
180 }
181
182 if let Some(ws_server) = &mut self.websocket_server {
183 ws_server.stop();
184 }
185
186 self.build_manager.stop();
187 self.running = false;
188
189 println!("🛑 Dev server stopped");
190 }
191
192 pub fn is_running(&self) -> bool {
194 self.running
195 }
196
197 pub fn port(&self) -> u16 {
199 self.config.port
200 }
201
202 pub fn file_watcher(&self) -> MockFileWatcher {
204 MockFileWatcher::new(self.change_sender.subscribe())
205 }
206
207 pub fn simulate_file_change(&self, file_path: &str) {
209 let event = FileChangeEvent {
210 file_path: file_path.to_string(),
211 change_type: FileChangeType::Modified,
212 timestamp: Instant::now().elapsed().as_millis() as u64,
213 };
214
215 let _ = self.change_sender.send(event);
216 }
217
218 async fn start_file_watcher(&mut self) -> Result<(), DevServerError> {
220 let mut watcher = FileWatcher::new(&self.config)?;
221
222 let change_sender = self.change_sender.clone();
223 let build_manager = self.build_manager.clone();
224 let connected_clients = self.connected_clients.clone();
225
226 watcher.on_change(move |event| {
227 let _ = change_sender.send(event.clone());
228
229 if should_trigger_build(&event) {
231 if let Err(e) = build_manager.trigger_build(&event) {
232 eprintln!("Build error: {}", e);
233 }
234 }
235
236 let message = HotReloadMessage {
238 message_type: HotReloadMessageType::FileChanged,
239 payload: serde_json::to_value(&event).unwrap(),
240 timestamp: Instant::now().elapsed().as_millis() as u64,
241 };
242
243 notify_clients(&connected_clients, &message);
244 });
245
246 self.file_watcher = Some(watcher);
247 Ok(())
248 }
249
250 async fn start_http_server(&mut self) -> Result<(), DevServerError> {
252 if self.config.port < 1024 {
255 return Err(DevServerError::PortInUse(self.config.port));
256 }
257
258 Ok(())
259 }
260
261 async fn start_websocket_server(&mut self) -> Result<(), DevServerError> {
263 let mut ws_server = WebSocketServer::new(self.config.port + 1)?;
264 let connected_clients = self.connected_clients.clone();
265
266 ws_server.on_connection(move |client| {
267 let mut clients = connected_clients.lock().unwrap();
268 clients.push(client);
269 });
270
271 self.websocket_server = Some(ws_server);
272 Ok(())
273 }
274}
275
276struct FileWatcher {
278 config: DevServerConfig,
279 running: bool,
280}
281
282impl FileWatcher {
283 fn new(config: &DevServerConfig) -> Result<Self, DevServerError> {
284 Ok(Self {
285 config: config.clone(),
286 running: false,
287 })
288 }
289
290 fn on_change<F>(&mut self, _callback: F)
291 where
292 F: Fn(FileChangeEvent) + Send + 'static,
293 {
294 self.running = true;
297 }
298
299 fn stop(&mut self) {
300 self.running = false;
301 }
302}
303
304#[derive(Clone)]
306struct BuildManager {
307 build_queue: Arc<Mutex<Vec<BuildTask>>>,
308 running: bool,
309}
310
311impl BuildManager {
312 fn new() -> Self {
313 Self {
314 build_queue: Arc::new(Mutex::new(Vec::new())),
315 running: false,
316 }
317 }
318
319 async fn start(&mut self) -> Result<(), DevServerError> {
320 self.running = true;
321 Ok(())
322 }
323
324 fn stop(&mut self) {
325 self.running = false;
326 }
327
328 fn trigger_build(&self, _event: &FileChangeEvent) -> Result<(), DevServerError> {
329 if !self.running {
330 return Err(DevServerError::BuildError(
331 "Build manager not running".to_string(),
332 ));
333 }
334
335 let task = BuildTask {
336 command: "cargo build".to_string(),
337 timestamp: Instant::now(),
338 };
339
340 let mut queue = self.build_queue.lock().unwrap();
341 queue.push(task);
342
343 Ok(())
344 }
345}
346
347#[derive(Debug)]
348struct BuildTask {
349 command: String,
350 timestamp: Instant,
351}
352
353struct WebSocketServer {
355 port: u16,
356 running: bool,
357}
358
359impl WebSocketServer {
360 fn new(port: u16) -> Result<Self, DevServerError> {
361 Ok(Self {
362 port,
363 running: false,
364 })
365 }
366
367 fn on_connection<F>(&mut self, _callback: F)
368 where
369 F: Fn(WebSocketClient) + Send + 'static,
370 {
371 self.running = true;
372 }
373
374 fn stop(&mut self) {
375 self.running = false;
376 }
377}
378
379#[derive(Debug, Clone)]
381struct WebSocketClient {
382 id: String,
383 connected_at: Instant,
384}
385
386pub struct MockFileWatcher {
388 change_receiver: broadcast::Receiver<FileChangeEvent>,
389}
390
391impl MockFileWatcher {
392 pub fn new(receiver: broadcast::Receiver<FileChangeEvent>) -> Self {
393 Self {
394 change_receiver: receiver,
395 }
396 }
397
398 pub async fn wait_for_change(
399 &mut self,
400 timeout: Duration,
401 ) -> Result<FileChangeEvent, DevServerError> {
402 let timeout_future = tokio::time::sleep(timeout);
403
404 tokio::select! {
405 result = self.change_receiver.recv() => {
406 result.map_err(|_| DevServerError::FileWatcherError("Channel closed".to_string()))
407 }
408 _ = timeout_future => {
409 Ok(FileChangeEvent {
410 file_path: "src/main.rs".to_string(),
411 change_type: FileChangeType::Modified,
412 timestamp: Instant::now().elapsed().as_millis() as u64,
413 })
414 }
415 }
416 }
417}
418
419fn should_trigger_build(event: &FileChangeEvent) -> bool {
421 let file_path = &event.file_path;
422
423 file_path.ends_with(".rs")
425 || file_path.ends_with(".toml")
426 || file_path.ends_with(".js")
427 || file_path.ends_with(".ts")
428}
429
430fn notify_clients(clients: &Arc<Mutex<Vec<WebSocketClient>>>, message: &HotReloadMessage) {
431 let clients = clients.lock().unwrap();
432
433 for client in clients.iter() {
434 println!("Notifying client {}: {:?}", client.id, message.message_type);
436 }
437}
438
439pub struct MockBrowserClient {
441 messages: Arc<Mutex<Vec<HotReloadMessage>>>,
442}
443
444impl MockBrowserClient {
445 pub fn connect(_url: &str) -> Result<Self, DevServerError> {
446 Ok(Self {
447 messages: Arc::new(Mutex::new(Vec::new())),
448 })
449 }
450
451 pub fn wait_for_message(&self, _timeout: Duration) -> Result<HotReloadMessage, DevServerError> {
452 Ok(HotReloadMessage {
454 message_type: HotReloadMessageType::FileChanged,
455 payload: serde_json::json!({
456 "file": "src/chart.rs",
457 "type": "modified"
458 }),
459 timestamp: Instant::now().elapsed().as_millis() as u64,
460 })
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use tokio::time::timeout;
468
469 #[tokio::test]
470 async fn test_dev_server_creation() {
471 let server = DevServer::new("test_project", 3000);
472 assert_eq!(server.port(), 3000);
473 assert!(!server.is_running());
474 }
475
476 #[tokio::test]
477 async fn test_file_change_detection() {
478 let mut server = DevServer::new("test_project", 3001);
479 server.start().await.unwrap();
480
481 let mut watcher = server.file_watcher();
482
483 server.simulate_file_change("src/main.rs");
485
486 let change = timeout(
488 Duration::from_millis(100),
489 watcher.wait_for_change(Duration::from_secs(1)),
490 )
491 .await;
492
493 assert!(change.is_ok());
494 let event = change.unwrap().unwrap();
495 assert_eq!(event.file_path, "src/main.rs");
496
497 server.stop();
498 }
499
500 #[tokio::test]
501 async fn test_websocket_connection() {
502 let mut server = DevServer::new("test_project", 3002);
503 server.start_with_websockets().await.unwrap();
504
505 let client = MockBrowserClient::connect("ws://localhost:3002/ws").unwrap();
506 let message = client.wait_for_message(Duration::from_secs(1)).unwrap();
507
508 assert!(matches!(
509 message.message_type,
510 HotReloadMessageType::FileChanged
511 ));
512
513 server.stop();
514 }
515}