1mod backend_compiling_server;
2mod dev_server;
3mod frontend_dev_server;
4
5pub mod controller;
6use cargo_metadata::CompilerMessage;
7use cargo_toml::Manifest;
8use serde::Serialize;
9use serde_json::json;
10mod endpoints;
11use crate::util::net::find_free_port;
12use async_priority_channel as priority;
13pub use endpoints::*;
14use std::iter::FromIterator;
15use std::path::PathBuf;
16use std::process::exit;
17use std::sync::Arc;
18use tokio::sync::Mutex;
19use watchexec::event::{Event, Priority, Tag};
20use watchexec::signal::source::worker;
21use watchexec::signal::source::MainSignal;
22
23#[cfg(windows)]
24const NPM: &str = "npm.cmd";
25
26#[cfg(not(windows))]
27const NPM: &str = "npm";
28
29pub async fn vitejs_ping_down() {
30 let Ok(port) = std::env::var("DEV_SERVER_PORT") else {
31 return;
32 };
33
34 let url = format!("http://localhost:{port}/vitejs-down");
35
36 match reqwest::get(url).await {
38 Ok(_) => {}
39 Err(_) => {
40 println!("WARNING: Could not inform dev server that vitejs is down.");
41 }
42 };
43}
44
45pub async fn vitejs_ping_up() {
46 let Ok(port) = std::env::var("DEV_SERVER_PORT") else {
47 return;
48 };
49
50 let url = format!("http://localhost:{port}/vitejs-up");
51
52 match reqwest::get(url).await {
54 Ok(_) => {}
55 Err(_) => {
56 println!("WARNING: Could not inform dev server that vitejs is up.");
57 }
58 };
59}
60
61pub async fn setup_development() {
62 let Ok(port) = std::env::var("DEV_SERVER_PORT") else {
63 return;
64 };
65
66 let url = format!("http://localhost:{port}/backend-up");
67
68 match reqwest::get(url).await {
70 Ok(_) => {}
71 Err(_) => {
72 println!("WARNING: Could not inform dev server of presence.");
73 }
74 };
75}
76
77#[allow(non_camel_case_types, clippy::module_name_repetitions)]
78#[derive(Debug, Clone)]
79pub enum DevServerEvent {
80 SHUTDOWN, CHECK_MIGRATIONS, PendingMigrations(bool, Vec<CreateRustAppMigration>),
92 MigrationResponse(bool, Option<String>),
93 FeaturesList(Vec<String>),
94 ViteJSStatus(bool),
95 BackendCompiling(bool),
96 BackendRestarting(bool),
97 BackendStatus(bool),
98 CompileSuccess(bool),
99 CompileMessages(Vec<CompilerMessage>),
100}
101
102#[derive(Serialize, Debug, Clone)]
103pub enum MigrationStatus {
104 Applied,
105 Pending,
106 AppliedButMissingLocally,
107 Unknown,
108}
109
110#[derive(Serialize, Debug, Clone)]
111pub struct CreateRustAppMigration {
112 name: String,
113 version: String,
114 status: MigrationStatus,
115}
116
117impl DevServerEvent {
118 #[must_use]
121 pub fn json(self) -> String {
122 match self {
123 Self::CHECK_MIGRATIONS => json!({
124 "type": "_",
125 })
126 .to_string(),
127 Self::PendingMigrations(migrations_pending, migrations) => json!({
128 "type": "migrationsPending",
129 "status": migrations_pending,
130 "migrations": migrations
131 })
132 .to_string(),
133 Self::MigrationResponse(success, error_message) => json!({
134 "type": "migrateResponse",
135 "status": success,
136 "error": error_message,
137 })
138 .to_string(),
139 Self::FeaturesList(list) => json!({
140 "type": "featuresList",
141 "features": list
142 })
143 .to_string(),
144 Self::ViteJSStatus(b) => json!({
145 "type": "viteStatus",
146 "status": b
147 })
148 .to_string(),
149 Self::CompileSuccess(b) => json!({
150 "type": "compileStatus",
151 "compiled": b
152 })
153 .to_string(),
154 Self::BackendCompiling(b) => json!({
155 "type": "backendCompiling",
156 "compiling": b
157 })
158 .to_string(),
159 Self::BackendStatus(b) => json!({
160 "type": "backendStatus",
161 "status": b
162 })
163 .to_string(),
164 Self::BackendRestarting(b) => json!({
165 "type": "backendRestarting",
166 "status": b
167 })
168 .to_string(),
169 Self::SHUTDOWN => json!({
170 "type": "backendStatus",
171 "status": "false"
172 })
173 .to_string(),
174 Self::CompileMessages(msgs) => {
175 let messages = serde_json::to_value(&msgs).unwrap();
176 json!({
177 "type": "compilerMessages",
178 "messages": messages
179 })
180 .to_string()
181 }
182 }
183 }
184}
185
186#[allow(clippy::module_name_repetitions)]
187#[derive(Debug)]
188pub struct DevState {
189 pub frontend_server_running: bool,
190 pub backend_server_running: bool,
191 pub watchexec_running: bool,
192}
193
194fn get_features(project_dir: &'static str) -> Vec<String> {
195 let manifest_path = PathBuf::from_iter([project_dir, "Cargo.toml"]);
196 let cargo_toml = Manifest::from_path(manifest_path.clone());
197
198 let cargo_toml = match cargo_toml {
199 Ok(manifest) => manifest,
200 Err(e) => {
201 panic!(
202 "Could not read Cargo.toml at {:#?}: {:#?}",
203 manifest_path.to_string_lossy(),
204 e
205 );
206 }
207 };
208
209 let mut deps = cargo_toml.dependencies;
210 if let Some(workspace) = cargo_toml.workspace {
211 deps.extend(workspace.dependencies);
213 }
214 let dep = deps.get("create-rust-app").unwrap_or_else(|| {
215 panic!(
216 "Expected \"{}\" to list 'create-rust-app' as a dependency.",
217 project_dir
218 )
219 });
220 let dep = dep.clone();
221
222 dep.req_features().to_vec()
223}
224
225pub fn run_server(project_dir: &'static str) {
231 clearscreen::clear().expect("failed to clear screen");
232
233 let features = get_features(project_dir);
234
235 println!("..................................");
236 println!(".. Starting development server ...");
237 println!("..................................");
238 let rt = tokio::runtime::Runtime::new().unwrap();
239
240 let dev_port = std::env::var("DEV_SERVER_PORT").map_or_else(
241 |_| {
242 find_free_port(60012..65535)
243 .expect("FATAL: Could not find a free port for the development server.")
244 },
245 |p| {
246 p.parse::<u16>()
247 .expect("Could not parse DEV_SERVER_PORT to u16")
248 },
249 );
250
251 rt.block_on(async move {
252 let state = Arc::new(Mutex::new(DevState {
253 backend_server_running: false,
254 frontend_server_running: false,
255 watchexec_running: false,
256 }));
257 let state2 = state.clone();
258
259 let (signal_tx, signal_rx) = tokio::sync::broadcast::channel::<DevServerEvent>(64);
261 let signal_rx2 = signal_tx.subscribe();
262
263 let (dev_server_events_s, dev_server_events_r) =
265 tokio::sync::broadcast::channel::<DevServerEvent>(64);
266 let dev_server_events_s2 = dev_server_events_s.clone();
267 let dev_server_events_s3 = dev_server_events_s.clone();
268
269 let (file_events_s, _) = tokio::sync::broadcast::channel::<String>(64);
271 let file_events_s2 = file_events_s.clone();
272
273 tokio::spawn(async move {
274 dev_server::start(
275 project_dir,
276 dev_port,
277 dev_server_events_r,
278 dev_server_events_s3,
279 file_events_s2,
280 features,
281 )
282 .await;
283 });
284 tokio::spawn(async move {
285 backend_compiling_server::start(
286 project_dir,
287 dev_port,
288 signal_rx2,
289 dev_server_events_s2,
290 state2,
291 file_events_s,
292 )
293 .await;
294 });
295 tokio::spawn(async move {
296 frontend_dev_server::start(
297 project_dir,
298 dev_port,
299 signal_rx,
300 dev_server_events_s.clone(),
301 state,
302 )
303 .await;
304 });
305
306 listen_for_signals(signal_tx).await;
307 });
308}
309
310fn check_exit(state: &DevState) {
311 if !state.backend_server_running && !state.frontend_server_running && !state.watchexec_running {
313 exit(0);
314 }
315}
316
317async fn listen_for_signals(signal_tx: tokio::sync::broadcast::Sender<DevServerEvent>) {
318 let (event_sender, event_receiver) = priority::bounded::<Event, Priority>(1024);
319 let (error_sender, mut error_receiver) = tokio::sync::mpsc::channel(64);
320
321 tokio::spawn(async move {
323 if let Some(error) = error_receiver.recv().await {
327 panic!(
328 "Error handling process signal:\n==============================\n{:#?}",
329 error
330 );
331 }
332 });
333
334 tokio::spawn(async move {
336 while let Ok((event, _)) = event_receiver.recv().await {
337 if event.tags.contains(&Tag::Signal(MainSignal::Terminate))
338 || event.tags.contains(&Tag::Signal(MainSignal::Interrupt))
339 {
340 signal_tx.send(DevServerEvent::SHUTDOWN).unwrap();
341 }
342 }
343 });
344
345 worker(error_sender, event_sender).await.unwrap();
346}