1use notify::{Event, EventKind, RecursiveMode, Watcher};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7pub struct FileWatcher {
8 watcher: notify::RecommendedWatcher,
9 watched_paths: Vec<PathBuf>,
10 #[allow(dead_code)]
11 debounce_ms: u64,
12}
13
14impl FileWatcher {
15 pub fn new(debounce_ms: u64) -> anyhow::Result<Self> {
16 let watcher =
17 notify::recommended_watcher(move |res: Result<Event, notify::Error>| match res {
18 Ok(event) => {
19 tracing::debug!("File event: {:?}", event);
20 }
21 Err(e) => {
22 tracing::error!("Watch error: {:?}", e);
23 }
24 })?;
25
26 Ok(Self {
27 watcher,
28 watched_paths: Vec::new(),
29 debounce_ms,
30 })
31 }
32
33 pub fn watch(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> {
34 let path = path.as_ref().to_path_buf();
35 if !path.exists() {
36 std::fs::create_dir_all(&path)?;
37 }
38 self.watcher.watch(&path, RecursiveMode::Recursive)?;
39 self.watched_paths.push(path);
40 Ok(())
41 }
42
43 pub fn unwatch(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> {
44 let path = path.as_ref();
45 self.watcher.unwatch(path)?;
46 self.watched_paths.retain(|p| p != path);
47 Ok(())
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ChangedFile {
53 pub path: PathBuf,
54 pub kind: FileChangeKind,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub enum FileChangeKind {
59 Created,
60 Modified,
61 Deleted,
62 Renamed,
63}
64
65impl From<EventKind> for FileChangeKind {
66 fn from(kind: EventKind) -> Self {
67 match kind {
68 EventKind::Create(_) => FileChangeKind::Created,
69 EventKind::Modify(_) => FileChangeKind::Modified,
70 EventKind::Remove(_) => FileChangeKind::Deleted,
71 _ => FileChangeKind::Modified,
72 }
73 }
74}
75
76pub struct HmrServer {
77 pub port: u16,
78 connections: Arc<RwLock<Vec<tokio::sync::mpsc::Sender<HmrMessage>>>>,
79}
80
81impl HmrServer {
82 pub fn new(port: u16) -> anyhow::Result<Self> {
83 Ok(Self {
84 port,
85 connections: Arc::new(RwLock::new(Vec::new())),
86 })
87 }
88
89 pub async fn notify_change(&self, files: Vec<ChangedFile>) {
90 let message = HmrMessage::Reload { files };
91 let connections = self.connections.write().await;
92
93 for tx in connections.iter() {
94 let _ = tx.try_send(message.clone());
95 }
96 }
97
98 pub async fn broadcast_message(&self, message: HmrMessage) {
99 let connections = self.connections.write().await;
100
101 for tx in connections.iter() {
102 let _ = tx.try_send(message.clone());
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(tag = "type")]
109pub enum HmrMessage {
110 Reload { files: Vec<ChangedFile> },
111 FullReload { reason: String },
112 ModuleUpdate { module: String, code: String },
113 Error { message: String },
114 Connected { client_id: String },
115}
116
117pub struct HmrClient {
118 #[allow(dead_code)]
119 ws_url: String,
120}
121
122impl HmrClient {
123 pub fn new(ws_url: &str) -> Self {
124 Self {
125 ws_url: ws_url.to_string(),
126 }
127 }
128
129 pub fn generate_script() -> String {
130 r#"
131(function() {
132 var ws = null;
133 var reconnectAttempts = 0;
134 var maxReconnectAttempts = 10;
135
136 function connect() {
137 var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
138 ws = new WebSocket(protocol + '//' + window.location.host + '/__hmr');
139
140 ws.onopen = function() {
141 console.log('[HMR] Connected to dev server');
142 reconnectAttempts = 0;
143 };
144
145 ws.onmessage = function(event) {
146 var data = JSON.parse(event.data);
147
148 switch (data.type) {
149 case 'reload':
150 console.log('[HMR] Reloading modules:', data.files);
151 if (window.__nestforgeWebHot) {
152 window.__nestforgeWebHot.invalidate();
153 } else {
154 window.location.reload();
155 }
156 break;
157 case 'fullReload':
158 console.log('[HMR] Full reload:', data.reason);
159 window.location.reload();
160 break;
161 case 'moduleUpdate':
162 console.log('[HMR] Module updated:', data.module);
163 break;
164 case 'error':
165 console.error('[HMR] Error:', data.message);
166 break;
167 }
168 };
169
170 ws.onclose = function() {
171 console.log('[HMR] Disconnected');
172 if (reconnectAttempts < maxReconnectAttempts) {
173 reconnectAttempts++;
174 setTimeout(connect, 1000 * reconnectAttempts);
175 }
176 };
177 }
178
179 connect();
180 window.__hmrClient = ws;
181})();
182"#
183 .to_string()
184 }
185}