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#[derive(Args, Debug, Clone)]
16pub struct DevOptions {
17 #[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
42pub fn dev(opts: DevOptions) -> Result<(), OsedaDevError> {
52 dev_with_shutdown(opts, Arc::new(AtomicBool::new(false)))
53}
54
55pub 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 .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 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}