Skip to main content

oseda_cli/cmd/
dev.rs

1use std::{
2    process::Command,
3    sync::{
4        atomic::{AtomicBool, Ordering},
5        Arc,
6    },
7    time::Duration,
8};
9
10use clap::Args;
11
12use crate::cmd::run::is_cwd_oseda_project;
13
14/// Options for the `oseda dev` command
15#[derive(Args, Debug, Clone)]
16pub struct DevOptions {
17    /// Port to run the vite dev server on
18    #[arg(long, default_value_t = 3000)]
19    pub port: u16,
20}
21
22#[derive(Debug)]
23pub enum OsedaDevError {
24    NotOsedaProjectError(String),
25    ServeError(String),
26}
27
28impl std::error::Error for OsedaDevError {}
29impl std::fmt::Display for OsedaDevError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Self::NotOsedaProjectError(msg) => write!(
33                f,
34                "Current working directory is not an Oseda project: {}",
35                msg
36            ),
37            Self::ServeError(msg) => write!(f, "Oseda Dev Server Error: {}", msg),
38        }
39    }
40}
41
42/// Run in dev mode, with auto-reload on save
43///
44/// # Arguments:
45/// * `opts` - options for subcommand
46/// * `shutdown_flag` - Arc to kill process
47///
48/// # Returns
49/// * `Ok()` on success
50/// * `Err` on any issue related to running oseda in dev mode
51pub fn dev(opts: DevOptions) -> Result<(), OsedaDevError> {
52    dev_with_shutdown(opts, Arc::new(AtomicBool::new(false)))
53}
54
55/// Run in dev mode, with auto-reload on save, with a shutdown flag
56///
57/// # Arguments:
58/// * `opts` - options for subcommand
59/// * `shutdown_flag` - Arc to kill process
60///
61/// # Returns
62/// * `Ok()` on success
63/// * `Err` on any issue related to running oseda in dev mode
64pub fn dev_with_shutdown(
65    opts: DevOptions,
66    shutdown_flag: Arc<AtomicBool>,
67) -> Result<(), OsedaDevError> {
68    if !is_cwd_oseda_project() {
69        return Err(OsedaDevError::NotOsedaProjectError(
70            "oseda-config.json not found".to_string(),
71        ));
72    }
73
74    let mut cmd = Command::new("npx");
75    cmd.arg("vite")
76        .arg("--port")
77        .arg(opts.port.to_string())
78        // fail if port not allowed
79        .arg("--strictPort");
80
81    let mut child = cmd.spawn().map_err(|e| {
82        println!("Error starting `npx vite`: {e}");
83        println!("Please ensure that `npx` and `vite` are installed and in your PATH.");
84        OsedaDevError::ServeError("failed to start vite dev server".into())
85    })?;
86
87    let ctrlc_flag = shutdown_flag.clone();
88    ctrlc::set_handler(move || {
89        println!("\nSIGINT received. Shutting down dev server...");
90        ctrlc_flag.store(true, Ordering::SeqCst);
91    })
92    .map_err(|e| {
93        println!("Error setting ctrl+c handler: {e}");
94        OsedaDevError::ServeError("failed to set handler".into())
95    })?;
96
97    // block until ctrl+c/shutdown flag OR the vite process dies on its own
98    while !shutdown_flag.load(Ordering::SeqCst) {
99        if let Ok(Some(status)) = child.try_wait() {
100            println!("`vite` exited on its own with status: {status}");
101            return Err(OsedaDevError::ServeError(
102                "vite dev server exited unexpectedly".into(),
103            ));
104        }
105        std::thread::sleep(Duration::from_millis(100));
106    }
107
108    if let Err(e) = child.kill() {
109        println!("Failed to kill `vite`: {e}");
110    } else {
111        println!("`vite` dev server terminated.");
112    }
113
114    let _ = child.wait();
115
116    Ok(())
117}