orbiton/commands/
dev.rs

1// Command for starting the development server
2
3use 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    /// Port to use for the development server
18    #[arg(short, long, default_value = "8000")]
19    port: u16,
20
21    /// Project directory
22    #[arg(short, long)]
23    dir: Option<PathBuf>,
24
25    /// Open in browser
26    #[arg(short, long)]
27    open: bool,
28
29    /// Use beta toolchain for building and testing
30    #[arg(long)]
31    beta: bool,
32}
33
34pub fn execute(args: DevArgs) -> Result<()> {
35    // Determine the project directory
36    let project_dir = match args.dir {
37        Some(dir) => dir,
38        None => std::env::current_dir()?,
39    };
40
41    // Load configuration from .orbiton.toml or use defaults
42    let mut config = OrbitonConfig::load_from_project(&project_dir)?;
43
44    // Override config with command line arguments
45    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    // Validate the configuration
53    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    // Create a development server using the configuration
69    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        // Verify beta toolchain is installed
77        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                    // Try to install beta toolchain
90                    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    // Start the server in a separate thread
111    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    // Open the browser if requested (use config or CLI args)
122    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    // Set up file watching
130    setup_file_watching(project_dir.as_path(), &server)?;
131
132    // Wait for Ctrl+C
133    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    // Keep the main thread running
140    loop {
141        std::thread::sleep(Duration::from_secs(1));
142    }
143}
144
145/// Rebuild the project using cargo
146///
147/// Returns true if the build was successful, false otherwise
148fn rebuild_project(project_dir: &Path, use_beta: bool) -> bool {
149    // Determine which toolchain to use
150    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    // Set up the build command with appropriate arguments
159    command
160        .arg("build")
161        .arg("--color=always")
162        .current_dir(project_dir);
163
164    // Execute the build command
165    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    // Create a watcher
193    let mut watcher =
194        notify::recommended_watcher(move |res: std::result::Result<Event, notify::Error>| {
195            match res {
196                Ok(event) => {
197                    // Handle file change event
198                    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    // Watch the project directory
207    watcher.watch(&watcher_dir, RecursiveMode::Recursive)?;
208
209    // Keep track of the watcher to prevent it from being dropped
210    std::thread::spawn(move || {
211        let _watcher = watcher; // Keep watcher alive
212        let pdir = project_dir.clone(); // Create a new binding for the project directory
213
214        // Debounce mechanism to avoid multiple rebuilds in quick succession
215        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            // Check if enough time has passed since last rebuild for additional debouncing
222            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            // Send the file change event to all connected clients
240            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            } // Track changed modules in HMR context for intelligent updates
250            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                    // Log which file triggered the update
256                    println!(
257                        "{} {}",
258                        style("File changed:").bold().blue(),
259                        style(&module).dim()
260                    );
261                }
262            }
263            // Determine if we should rebuild using HMR context debouncing
264            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                // Send rebuild start notification using dev server method
275                if let Err(e) = server.send_rebuild_status("started") {
276                    error!("Failed to send rebuild start status: {e}");
277                }
278
279                // Perform the actual rebuild
280                let rebuild_status = rebuild_project(&pdir, server.is_using_beta());
281
282                // Report the rebuild status
283                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                // Send the rebuild status using dev server method
299                if let Err(e) = server.send_rebuild_status(status) {
300                    error!("Failed to send rebuild status: {e}");
301                }
302
303                // If rebuild succeeded, record the rebuild and send HMR updates
304                if rebuild_status {
305                    // Record successful rebuild
306                    hmr_context.record_rebuild();
307
308                    // Get affected modules from HMR context
309                    let affected_modules = hmr_context.get_pending_updates();
310
311                    if !affected_modules.is_empty() {
312                        // Log the modules being updated
313                        println!(
314                            "{} HMR update for modules: {}",
315                            style("Sending").bold().blue(),
316                            style(affected_modules.join(", ")).italic()
317                        );
318
319                        // Send HMR update using dev server method
320                        if let Err(e) = server.send_hmr_update(affected_modules) {
321                            error!("Failed to send HMR update: {e}");
322                        }
323                    }
324                } else {
325                    // On rebuild failure, send reload command to refresh the page
326                    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}