Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-core/src/hot_reload.rs
StratoSDK / crates / strato-core / src / hot_reload.rs
1//! Hot reload and live preview system for StratoUI
2//!
3//! Provides file watching, code reloading, and live preview capabilities
4//! for rapid development and iteration
5 
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8use std::time::SystemTime;
9 
10#[cfg(feature = "hot-reload")]
11use futures_util::{SinkExt, StreamExt};
12#[cfg(feature = "hot-reload")]
13use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
14#[cfg(feature = "hot-reload")]
15use tokio::sync::mpsc;
16#[cfg(feature = "hot-reload")]
17use tokio_tungstenite::{accept_async, tungstenite::Message};
18 
19/// File change event types
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ChangeType {
22 Created,
23 Modified,
24 Deleted,
25 Renamed { from: PathBuf, to: PathBuf },
26}
27 
28/// File change event
29#[derive(Debug, Clone)]
30pub struct FileChange {
31 pub path: PathBuf,
32 pub change_type: ChangeType,
33 pub timestamp: SystemTime,
34}
35 
36/// Hot reload configuration
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct HotReloadConfig {
39 /// Directories to watch for changes
40 pub watch_dirs: Vec<PathBuf>,
41 /// File extensions to watch
42 pub watch_extensions: Vec<String>,
43 /// Debounce delay in milliseconds
44 pub debounce_ms: u64,
45 /// Enable hot reload
46 pub enabled: bool,
47 /// Enable live preview
48 pub live_preview: bool,
49 /// Preview server port
50 pub preview_port: u16,
51}
52 
53impl Default for HotReloadConfig {
54 fn default() -> Self {
55 Self {
56 watch_dirs: vec![PathBuf::from("src"), PathBuf::from("assets")],
57 watch_extensions: vec![
58 "rs".to_string(),
59 "toml".to_string(),
60 "css".to_string(),
61 "js".to_string(),
62 "html".to_string(),
63 "png".to_string(),
64 "jpg".to_string(),
65 "svg".to_string(),
66 ],
67 debounce_ms: 300,
68 enabled: true,
69 live_preview: true,
70 preview_port: 3000,
71 }
72 }
73}
74 
75/// Hot reload event handler
76pub trait HotReloadHandler: Send + Sync {
77 /// Handle file changes
78 fn handle_change(&self, change: &FileChange) -> Result<(), Box<dyn std::error::Error>>;
79 
80 /// Handle compilation errors
81 fn handle_error(&self, error: &str);
82 
83 /// Handle successful reload
84 fn handle_reload_success(&self);
85}
86 
87/// File watcher for hot reload
88#[cfg(feature = "hot-reload")]
89pub struct FileWatcher {
90 config: HotReloadConfig,
91 watcher: Option<RecommendedWatcher>,
92 handlers: Arc<RwLock<Vec<Arc<dyn HotReloadHandler>>>>,
93 file_cache: Arc<RwLock<HashMap<PathBuf, SystemTime>>>,
94 debounce_cache: Arc<Mutex<HashMap<PathBuf, SystemTime>>>,
95}
96 
97#[cfg(feature = "hot-reload")]
98impl FileWatcher {
99 /// Create a new file watcher
100 pub fn new(config: HotReloadConfig) -> Result<Self, Box<dyn std::error::Error>> {
101 Ok(Self {
102 config,
103 watcher: None,
104 handlers: Arc::new(RwLock::new(Vec::new())),
105 file_cache: Arc::new(RwLock::new(HashMap::new())),
106 debounce_cache: Arc::new(Mutex::new(HashMap::new())),
107 })
108 }
109 
110 /// Add a hot reload handler
111 pub fn add_handler(&self, handler: Arc<dyn HotReloadHandler>) {
112 self.handlers.write().push(handler);
113 }
114 
115 /// Start watching for file changes
116 pub async fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
117 if !self.config.enabled {
118 return Ok(());
119 }
120 
121 let (tx, mut rx) = mpsc::channel(100);
122 let handlers = Arc::clone(&self.handlers);
123 let file_cache = Arc::clone(&self.file_cache);
124 let debounce_cache = Arc::clone(&self.debounce_cache);
125 let debounce_duration = Duration::from_millis(self.config.debounce_ms);
126 let watch_extensions = self.config.watch_extensions.clone();
127 
128 // Create file watcher
129 let mut watcher =
130 notify::recommended_watcher(move |res: Result<Event, notify::Error>| match res {
131 Ok(event) => {
132 if let Err(e) = tx.blocking_send(event) {
133 eprintln!("Failed to send file event: {}", e);
134 }
135 }
136 Err(e) => eprintln!("File watcher error: {}", e),
137 })?;
138 
139 // Watch configured directories
140 for dir in &self.config.watch_dirs {
141 if dir.exists() {
142 watcher.watch(dir, RecursiveMode::Recursive)?;
143 println!("Watching directory: {}", dir.display());
144 }
145 }
146 
147 self.watcher = Some(watcher);
148 
149 // Spawn event processing task
150 tokio::spawn(async move {
151 while let Some(event) = rx.recv().await {
152 Self::process_event(
153 event,
154 &handlers,
155 &file_cache,
156 &debounce_cache,
157 debounce_duration,
158 &watch_extensions,
159 )
160 .await;
161 }
162 });
163 
164 println!("Hot reload system started");
165 Ok(())
166 }
167 
168 /// Process a file system event
169 async fn process_event(
170 event: Event,
171 handlers: &Arc<RwLock<Vec<Arc<dyn HotReloadHandler>>>>,
172 file_cache: &Arc<RwLock<HashMap<PathBuf, SystemTime>>>,
173 debounce_cache: &Arc<Mutex<HashMap<PathBuf, SystemTime>>>,
174 debounce_duration: Duration,
175 watch_extensions: &[String],
176 ) {
177 let now = SystemTime::now();
178 
179 for path in event.paths {
180 // Check if file extension is watched
181 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
182 if !watch_extensions.contains(&ext.to_string()) {
183 continue;
184 }
185 }
186 
187 // Debounce file changes
188 {
189 let mut debounce = debounce_cache.lock();
190 if let Some(&last_time) = debounce.get(&path) {
191 if now.duration_since(last_time).unwrap_or_default() < debounce_duration {
192 continue;
193 }
194 }
195 debounce.insert(path.clone(), now);
196 }
197 
198 let change_type = match event.kind {
199 EventKind::Create(_) => ChangeType::Created,
200 EventKind::Modify(_) => ChangeType::Modified,
201 EventKind::Remove(_) => ChangeType::Deleted,
202 _ => continue,
203 };
204 
205 let file_change = FileChange {
206 path: path.clone(),
207 change_type,
208 timestamp: now,
209 };
210 
211 // Update file cache
212 match file_change.change_type {
213 ChangeType::Created | ChangeType::Modified => {
214 file_cache.write().insert(path.clone(), now);
215 }
216 ChangeType::Deleted => {
217 file_cache.write().remove(&path);
218 }
219 _ => {}
220 }
221 
222 // Notify handlers
223 let handlers_read = handlers.read();
224 for handler in handlers_read.iter() {
225 if let Err(e) = handler.handle_change(&file_change) {
226 handler.handle_error(&format!("Hot reload error: {}", e));
227 } else {
228 handler.handle_reload_success();
229 }
230 }
231 }
232 }
233 
234 /// Stop watching for file changes
235 pub fn stop(&mut self) {
236 self.watcher = None;
237 println!("Hot reload system stopped");
238 }
239}
240 
241/// Live preview server for hot reload
242#[cfg(feature = "hot-reload")]
243pub struct LivePreviewServer {
244 config: HotReloadConfig,
245 clients: Arc<RwLock<Vec<mpsc::UnboundedSender<String>>>>,
246}
247 
248#[cfg(feature = "hot-reload")]
249impl LivePreviewServer {
250 /// Create a new live preview server
251 pub fn new(config: HotReloadConfig) -> Self {
252 Self {
253 config,
254 clients: Arc::new(RwLock::new(Vec::new())),
255 }
256 }
257 
258 /// Start the live preview server
259 pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
260 if !self.config.live_preview {
261 return Ok(());
262 }
263 
264 let clients = Arc::clone(&self.clients);
265 let port = self.config.preview_port;
266 
267 tokio::spawn(async move {
268 Self::run_server(port, clients).await;
269 });
270 
271 println!("Live preview server started on port {}", port);
272 Ok(())
273 }
274 
275 /// Run the WebSocket server
276 async fn run_server(port: u16, clients: Arc<RwLock<Vec<mpsc::UnboundedSender<String>>>>) {
277 use futures_util::{SinkExt, StreamExt};
278 use tokio::net::TcpListener;
279 use tokio_tungstenite::{accept_async, tungstenite::Message};
280 
281 let addr = format!("127.0.0.1:{}", port);
282 let listener = match TcpListener::bind(&addr).await {
283 Ok(listener) => listener,
284 Err(e) => {
285 eprintln!("Failed to bind to {}: {}", addr, e);
286 return;
287 }
288 };
289 
290 while let Ok((stream, _)) = listener.accept().await {
291 let clients = Arc::clone(&clients);
292 
293 tokio::spawn(async move {
294 let ws_stream = match accept_async(stream).await {
295 Ok(ws) => ws,
296 Err(e) => {
297 eprintln!("WebSocket connection error: {}", e);
298 return;
299 }
300 };
301 
302 let (mut ws_sender, mut ws_receiver) = ws_stream.split();
303 let (tx, mut rx) = mpsc::unbounded_channel();
304 
305 // Add client to list
306 clients.write().push(tx);
307 
308 // Handle incoming messages
309 let clients_clone = Arc::clone(&clients);
310 tokio::spawn(async move {
311 while let Some(msg) = ws_receiver.next().await {
312 match msg {
313 Ok(Message::Text(text)) => {
314 println!("Received: {}", text);
315 }
316 Ok(Message::Close(_)) => break,
317 Err(e) => {
318 eprintln!("WebSocket error: {}", e);
319 break;
320 }
321 _ => {}
322 }
323 }
324 
325 // Remove client when disconnected
326 clients_clone.write().clear(); // Simplified cleanup
327 });
328 
329 // Send messages to client
330 while let Some(message) = rx.recv().await {
331 if let Err(e) = ws_sender.send(Message::Text(message)).await {
332 eprintln!("Failed to send message: {}", e);
333 break;
334 }
335 }
336 });
337 }
338 }
339 
340 /// Broadcast a message to all connected clients
341 pub fn broadcast(&self, message: &str) {
342 let clients = self.clients.read();
343 for client in clients.iter() {
344 let _ = client.send(message.to_string());
345 }
346 }
347 
348 /// Notify clients of a file change
349 pub fn notify_change(&self, change: &FileChange) {
350 let message = serde_json::json!({
351 "type": "file_change",
352 "path": change.path,
353 "change_type": format!("{:?}", change.change_type),
354 "timestamp": change.timestamp.duration_since(SystemTime::UNIX_EPOCH)
355 .unwrap_or_default().as_secs()
356 });
357 
358 self.broadcast(&message.to_string());
359 }
360 
361 /// Notify clients of a reload
362 pub fn notify_reload(&self) {
363 let message = serde_json::json!({
364 "type": "reload",
365 "timestamp": SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
366 .unwrap_or_default().as_secs()
367 });
368 
369 self.broadcast(&message.to_string());
370 }
371}
372 
373/// Hot reload manager that coordinates file watching and live preview
374#[cfg(feature = "hot-reload")]
375pub struct HotReloadManager {
376 file_watcher: FileWatcher,
377 preview_server: LivePreviewServer,
378 config: HotReloadConfig,
379}
380 
381#[cfg(feature = "hot-reload")]
382impl HotReloadManager {
383 /// Create a new hot reload manager
384 pub fn new(config: HotReloadConfig) -> Result<Self, Box<dyn std::error::Error>> {
385 let file_watcher = FileWatcher::new(config.clone())?;
386 let preview_server = LivePreviewServer::new(config.clone());
387 
388 Ok(Self {
389 file_watcher,
390 preview_server,
391 config,
392 })
393 }
394 
395 /// Start the hot reload system
396 pub async fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
397 if !self.config.enabled {
398 return Ok(());
399 }
400 
401 // Start preview server
402 self.preview_server.start().await?;
403 
404 // Add preview server as a handler
405 let preview_handler = PreviewHandler::new(self.preview_server.clients.clone());
406 self.file_watcher.add_handler(Arc::new(preview_handler));
407 
408 // Start file watcher
409 self.file_watcher.start().await?;
410 
411 println!("Hot reload manager started");
412 Ok(())
413 }
414 
415 /// Stop the hot reload system
416 pub fn stop(&mut self) {
417 self.file_watcher.stop();
418 println!("Hot reload manager stopped");
419 }
420 
421 /// Get the preview server
422 pub fn preview_server(&self) -> &LivePreviewServer {
423 &self.preview_server
424 }
425}
426 
427/// Handler that integrates with the live preview server
428#[cfg(feature = "hot-reload")]
429struct PreviewHandler {
430 clients: Arc<RwLock<Vec<mpsc::UnboundedSender<String>>>>,
431}
432 
433#[cfg(feature = "hot-reload")]
434impl PreviewHandler {
435 fn new(clients: Arc<RwLock<Vec<mpsc::UnboundedSender<String>>>>) -> Self {
436 Self { clients }
437 }
438}
439 
440#[cfg(feature = "hot-reload")]
441impl HotReloadHandler for PreviewHandler {
442 fn handle_change(&self, change: &FileChange) -> Result<(), Box<dyn std::error::Error>> {
443 let message = serde_json::json!({
444 "type": "file_change",
445 "path": change.path,
446 "change_type": format!("{:?}", change.change_type),
447 "timestamp": change.timestamp.duration_since(SystemTime::UNIX_EPOCH)
448 .unwrap_or_default().as_secs()
449 });
450 
451 let clients = self.clients.read();
452 for client in clients.iter() {
453 let _ = client.send(message.to_string());
454 }
455 
456 Ok(())
457 }
458 
459 fn handle_error(&self, error: &str) {
460 let message = serde_json::json!({
461 "type": "error",
462 "message": error,
463 "timestamp": SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
464 .unwrap_or_default().as_secs()
465 });
466 
467 let clients = self.clients.read();
468 for client in clients.iter() {
469 let _ = client.send(message.to_string());
470 }
471 }
472 
473 fn handle_reload_success(&self) {
474 let message = serde_json::json!({
475 "type": "reload_success",
476 "timestamp": SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
477 .unwrap_or_default().as_secs()
478 });
479 
480 let clients = self.clients.read();
481 for client in clients.iter() {
482 let _ = client.send(message.to_string());
483 }
484 }
485}
486 
487/// Utility functions for hot reload
488pub mod utils {
489 use super::*;
490 
491 /// Check if a file should be watched based on extension
492 pub fn should_watch_file(path: &Path, extensions: &[String]) -> bool {
493 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
494 extensions.contains(&ext.to_string())
495 } else {
496 false
497 }
498 }
499 
500 /// Get the relative path from a base directory
501 pub fn get_relative_path(path: &Path, base: &Path) -> Option<PathBuf> {
502 path.strip_prefix(base).ok().map(|p| p.to_path_buf())
503 }
504 
505 /// Check if a path is in any of the watched directories
506 pub fn is_in_watched_dirs(path: &Path, watch_dirs: &[PathBuf]) -> bool {
507 watch_dirs.iter().any(|dir| path.starts_with(dir))
508 }
509}
510 
511#[cfg(test)]
512mod tests {
513 use super::*;
514 
515 #[test]
516 fn test_hot_reload_config() {
517 let config = HotReloadConfig::default();
518 assert!(config.enabled);
519 assert!(config.live_preview);
520 assert_eq!(config.preview_port, 3000);
521 assert_eq!(config.debounce_ms, 300);
522 }
523 
524 #[test]
525 fn test_file_change() {
526 let change = FileChange {
527 path: PathBuf::from("test.rs"),
528 change_type: ChangeType::Modified,
529 timestamp: SystemTime::now(),
530 };
531 
532 assert_eq!(change.path, PathBuf::from("test.rs"));
533 assert_eq!(change.change_type, ChangeType::Modified);
534 }
535 
536 #[test]
537 fn test_should_watch_file() {
538 let extensions = vec!["rs".to_string(), "toml".to_string()];
539 
540 assert!(utils::should_watch_file(Path::new("main.rs"), &extensions));
541 assert!(utils::should_watch_file(
542 Path::new("Cargo.toml"),
543 &extensions
544 ));
545 assert!(!utils::should_watch_file(
546 Path::new("README.md"),
547 &extensions
548 ));
549 }
550 
551 #[tokio::test]
552 #[cfg(feature = "hot-reload")]
553 async fn test_file_watcher_creation() {
554 let config = HotReloadConfig::default();
555 let watcher = FileWatcher::new(config);
556 assert!(watcher.is_ok());
557 }
558 
559 #[test]
560 #[cfg(feature = "hot-reload")]
561 fn test_live_preview_server_creation() {
562 let config = HotReloadConfig::default();
563 let server = LivePreviewServer::new(config);
564 assert_eq!(server.clients.read().len(), 0);
565 }
566 
567 #[tokio::test]
568 #[cfg(feature = "hot-reload")]
569 async fn test_hot_reload_manager() {
570 let mut config = HotReloadConfig::default();
571 config.enabled = false; // Disable for testing
572 
573 let manager = HotReloadManager::new(config);
574 assert!(manager.is_ok());
575 }
576}
577