1use anyhow::Result;
4use clap::Args;
5use console::style;
6use log::{debug, error, info};
7use notify::{Event, RecursiveMode, Watcher};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use crate::config::OrbitonConfig;
13use crate::dev_server::DevServer;
14
15#[derive(Args)]
16pub struct DevArgs {
17 #[arg(short, long, default_value = "8000")]
19 port: u16,
20
21 #[arg(short, long)]
23 dir: Option<PathBuf>,
24
25 #[arg(short, long)]
27 open: bool,
28
29 #[arg(long)]
31 beta: bool,
32}
33
34pub fn execute(args: DevArgs) -> Result<()> {
35 let project_dir = match args.dir {
37 Some(dir) => dir,
38 None => std::env::current_dir()?,
39 };
40
41 let mut config = OrbitonConfig::load_from_project(&project_dir)?;
43
44 if args.port != 8000 {
46 config.dev_server.port = args.port;
47 }
48 if args.beta {
49 config.build.use_beta_toolchain = true;
50 }
51
52 config.validate()?;
54
55 if config.build.use_beta_toolchain {
56 println!(
57 "{} development server with {} toolchain for project at {project_dir:?}",
58 style("Starting").bold().green(),
59 style("beta").bold().yellow()
60 );
61 } else {
62 println!(
63 "{} development server for project at {project_dir:?}",
64 style("Starting").bold().green()
65 );
66 }
67
68 let mut server = DevServer::new_with_options(
70 config.dev_server.port,
71 &project_dir,
72 config.build.use_beta_toolchain,
73 )?;
74
75 if config.build.use_beta_toolchain {
76 match std::process::Command::new("rustup")
78 .args(["toolchain", "list"])
79 .output()
80 {
81 Ok(output) => {
82 let output_str = String::from_utf8_lossy(&output.stdout);
83 if !output_str.contains("beta") {
84 println!(
85 "{} Beta toolchain not installed. Installing...",
86 style("Warning:").bold().yellow()
87 );
88
89 let install_result = std::process::Command::new("rustup")
91 .args(["toolchain", "install", "beta"])
92 .status();
93
94 if let Err(e) = install_result {
95 return Err(anyhow::anyhow!("Failed to install beta toolchain: {}", e));
96 }
97 }
98 }
99 Err(e) => {
100 return Err(anyhow::anyhow!("Failed to check for beta toolchain: {}", e));
101 }
102 }
103
104 println!(
105 "{} Using Rust beta toolchain for builds",
106 style("Info:").bold().blue()
107 );
108 }
109
110 let _server_handle = server.start()?;
112
113 println!(
114 "Development server running at {}",
115 style(format!("http://localhost:{}", config.dev_server.port))
116 .bold()
117 .blue()
118 .underlined()
119 );
120
121 let should_open = args.open || config.dev_server.auto_open;
123 if should_open {
124 if let Err(e) = open::that(format!("http://localhost:{}", config.dev_server.port)) {
125 error!("Failed to open browser: {e}");
126 }
127 }
128
129 setup_file_watching(project_dir.as_path(), &server)?;
131
132 println!("Press {} to stop the server", style("Ctrl+C").bold());
134 ctrlc::set_handler(move || {
135 println!("\n{} development server", style("Stopping").bold().red());
136 std::process::exit(0);
137 })?;
138
139 loop {
141 std::thread::sleep(Duration::from_secs(1));
142 }
143}
144
145fn rebuild_project(project_dir: &Path, use_beta: bool) -> bool {
149 let mut command = if use_beta {
151 let mut cmd = std::process::Command::new("cargo");
152 cmd.arg("+beta");
153 cmd
154 } else {
155 std::process::Command::new("cargo")
156 };
157
158 command
160 .arg("build")
161 .arg("--color=always")
162 .current_dir(project_dir);
163
164 debug!("Running build command: {:?}", command);
166
167 match command.status() {
168 Ok(status) => {
169 if status.success() {
170 info!("Project rebuilt successfully");
171 true
172 } else {
173 error!("Project rebuild failed with status: {}", status);
174 false
175 }
176 }
177 Err(e) => {
178 error!("Failed to execute build command: {}", e);
179 false
180 }
181 }
182}
183
184fn setup_file_watching(project_dir: &Path, server: &DevServer) -> Result<()> {
185 let (tx, rx) = std::sync::mpsc::channel();
186 let server = server.clone();
187 let project_dir = project_dir.to_path_buf();
188 let watcher_dir = project_dir.clone();
189 let log_dir = project_dir.clone();
190 let hmr_context = Arc::clone(server.hmr_context());
191
192 let mut watcher =
194 notify::recommended_watcher(move |res: std::result::Result<Event, notify::Error>| {
195 match res {
196 Ok(event) => {
197 if let Err(e) = tx.send(event) {
199 error!("Failed to send file change event: {e}");
200 }
201 }
202 Err(e) => error!("Watch error: {e}"),
203 }
204 })?;
205
206 watcher.watch(&watcher_dir, RecursiveMode::Recursive)?;
208
209 std::thread::spawn(move || {
211 let _watcher = watcher; let pdir = project_dir.clone(); let mut last_rebuild = std::time::Instant::now();
216 const DEBOUNCE_TIME: Duration = Duration::from_millis(500);
217
218 for event in rx {
219 debug!("File change event: {event:?}");
220
221 let now = std::time::Instant::now();
223 if now.duration_since(last_rebuild) < DEBOUNCE_TIME {
224 debug!("Skipping event due to debounce (last rebuild too recent)");
225 continue;
226 }
227
228 let paths = event
229 .paths
230 .iter()
231 .map(|p| {
232 p.strip_prefix(&pdir)
233 .unwrap_or(p)
234 .to_string_lossy()
235 .into_owned()
236 })
237 .collect::<Vec<_>>();
238
239 let message = serde_json::json!({
241 "type": "fileChange",
242 "paths": paths,
243 "kind": format!("{:?}", event.kind)
244 })
245 .to_string();
246
247 if let Err(e) = server.broadcast_update(message) {
248 error!("Failed to broadcast file change: {e}");
249 } let mut changed_modules = Vec::new();
251 for path in &event.paths {
252 if let Some(module) = hmr_context.record_file_change(path) {
253 changed_modules.push(module.clone());
254
255 println!(
257 "{} {}",
258 style("File changed:").bold().blue(),
259 style(&module).dim()
260 );
261 }
262 }
263 let should_rebuild = hmr_context.should_rebuild(DEBOUNCE_TIME);
265
266 if should_rebuild {
267 last_rebuild = now;
268
269 println!(
270 "{} project due to file changes",
271 style("Rebuilding").bold().yellow()
272 );
273
274 if let Err(e) = server.send_rebuild_status("started") {
276 error!("Failed to send rebuild start status: {e}");
277 }
278
279 let rebuild_status = rebuild_project(&pdir, server.is_using_beta());
281
282 let status = match rebuild_status {
284 true => "completed",
285 false => "failed",
286 };
287
288 println!(
289 "{} {}",
290 style("Rebuild").bold(),
291 if rebuild_status {
292 style("completed successfully").green()
293 } else {
294 style("failed").red()
295 }
296 );
297
298 if let Err(e) = server.send_rebuild_status(status) {
300 error!("Failed to send rebuild status: {e}");
301 }
302
303 if rebuild_status {
305 hmr_context.record_rebuild();
307
308 let affected_modules = hmr_context.get_pending_updates();
310
311 if !affected_modules.is_empty() {
312 println!(
314 "{} HMR update for modules: {}",
315 style("Sending").bold().blue(),
316 style(affected_modules.join(", ")).italic()
317 );
318
319 if let Err(e) = server.send_hmr_update(affected_modules) {
321 error!("Failed to send HMR update: {e}");
322 }
323 }
324 } else {
325 if let Err(e) = server.send_reload_command() {
327 error!("Failed to send reload command: {e}");
328 }
329 }
330 }
331 }
332 });
333
334 info!("File watching set up for {log_dir:?}");
335 Ok(())
336}