create_rust_app/dev/
mod.rs

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    // send event to dev server that the backend is up!
37    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    // send event to dev server that the backend is up!
53    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    // send event to dev server that the backend is up!
69    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    /*
81        These events are internal: they shouldn't be used to communicate with the web browser client
82    */
83    /// sent when Ctrl+C or similar signals are sent to the dev server parent process
84    SHUTDOWN, // related to external event: "BackendStatus"
85    /// sent once when the backend compiles successfully and more when files in the migrations/ directory change
86    CHECK_MIGRATIONS, // related to external event: "PendingMigrations"
87
88    /*
89        These events are external: they are sent to the web browser client
90    */
91    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    /// # Panics
119    /// * if the event cannot be serialized to JSON
120    #[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        // if the manifest has a workspace table, also read dependencies in there
212        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
225/// # Panics
226/// * if the project directory doesn't exist
227/// * if the project directory doesn't contain a Cargo.toml file
228/// * if the Cargo.toml file doesn't list `create-rust-app` as a dependency
229/// * cannot start a tokio runtime
230pub 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        // used for shutdown only (TODO: merge this with next broadcast channel)
260        let (signal_tx, signal_rx) = tokio::sync::broadcast::channel::<DevServerEvent>(64);
261        let signal_rx2 = signal_tx.subscribe();
262
263        // used for websocket events
264        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        // HACK: used to ignore some file modification events as a result of interaction with the dev server
270        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    // println!("Checking exit status {:#?}", state);
312    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    // panic on errors
322    tokio::spawn(async move {
323        // we panic on the first error we receive, so we only need to recv once
324        // if, in the future, we change the error handling to be more robust (not panic), we'll need to loop here
325        // like `while let Some(error) = error_receiver.recv().await { ... }`
326        if let Some(error) = error_receiver.recv().await {
327            panic!(
328                "Error handling process signal:\n==============================\n{:#?}",
329                error
330            );
331        }
332    });
333
334    // broadcast signals
335    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}