use crate::{ styles::{GLOW_STYLE, LINK_STYLE}, AppBuilder, BuildId, BuildMode, BuilderUpdate, BundleFormat, Result, ServeArgs, TraceController, }; mod ansi_buffer; mod output; mod proxy; mod proxy_ws; mod runner; mod server; mod update; use anyhow::bail; use dioxus_dx_wire_format::BuildStage; pub(crate) use output::*; pub(crate) use runner::*; pub(crate) use server::*; pub(crate) use update::*; /// For *all* builds, the CLI spins up a dedicated webserver, file watcher, and build infrastructure to serve the project. /// /// This includes web, desktop, mobile, fullstack, etc. /// /// Platform specifics: /// ------------------- /// - Web: We need to attach a filesystem server to our devtools webserver to serve the project. We /// want to emulate GithubPages here since most folks are deploying there and expect things like /// basepath to match. /// - Desktop: We spin up the dev server but without a filesystem server. /// - Mobile: Basically the same as desktop. /// /// When fullstack is enabled, we'll also build for the `server` target and then hotreload the server. /// The "server" is special here since "fullstack" is functionally just an addition to the regular client /// setup. /// /// Todos(Jon): /// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly. /// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server /// to a dynamic one on the fly. pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Result<()> { // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc let exit_on_error = args.exit_on_error; let mut builder = AppServer::new(args).await?; let mut devserver = WebServer::start(&builder)?; let mut screen = Output::start(builder.interactive).await?; // This is our default splash screen. We might want to make this a fancier splash screen in the future // Also, these commands might not be the most important, but it's all we've got enabled right now tracing::info!( r#"----------------------------------------------------------------- Serving your app: {binname}! 🚀 • Press {GLOW_STYLE}`ctrl+c`{GLOW_STYLE:#} to exit the server • Press {GLOW_STYLE}`r`{GLOW_STYLE:#} to rebuild the app • Press {GLOW_STYLE}`p`{GLOW_STYLE:#} to toggle automatic rebuilds • Press {GLOW_STYLE}`v`{GLOW_STYLE:#} to toggle verbose logging • Press {GLOW_STYLE}`/`{GLOW_STYLE:#} for more commands and shortcuts{extra} ----------------------------------------------------------------"#, binname = builder.client.build.executable_name(), extra = if builder.client.build.using_dioxus_explicitly { format!( "\n Learn more at {LINK_STYLE}https://dioxuslabs.com/learn/0.7/getting_started{LINK_STYLE:#}" ) } else { String::new() } ); builder.initialize(); loop { // Draw the state of the server to the screen screen.render(&builder, &devserver); // And then wait for any updates before redrawing let msg = tokio::select! { msg = builder.wait() => msg, msg = devserver.wait() => msg, msg = screen.wait() => msg, msg = tracer.wait() => msg, }; match msg { ServeUpdate::FilesChanged { files } => { if files.is_empty() || !builder.hot_reload { continue; } builder.handle_file_change(&files, &mut devserver).await; } ServeUpdate::RequestRebuild => { // The spacing here is important-ish: we want // `Full rebuild:` to line up with // `Hotreloading:` to keep the alignment during long edit sessions // `Hot-patching:` to keep the alignment during long edit sessions tracing::info!("Full rebuild: triggered manually"); builder.full_rebuild().await; devserver.send_reload_start().await; devserver.start_build().await; } // Run the server in the background // Waiting for updates here lets us tap into when clients are added/removed ServeUpdate::NewConnection { id, aslr_reference, pid, } => { devserver .send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT)) .await; if builder.server.is_some() { devserver .send_hotreload(builder.applied_hot_reload_changes(BuildId::SERVER)) .await; } builder.client_connected(id, aslr_reference, pid).await; } // Received a message from the devtools server - currently we only use this for // logging, so we just forward it the tui ServeUpdate::WsMessage { msg, bundle } => { screen.push_ws_message(bundle, &msg); } // Wait for logs from the build engine // These will cause us to update the screen // We also can check the status of the builds here in case we have multiple ongoing builds ServeUpdate::BuilderUpdate { id, update } => { let bundle_format = builder.get_build(id).unwrap().build.bundle; // Queue any logs to be printed if need be screen.new_build_update(&update); // And then update the websocketed clients with the new build status in case they want it devserver.new_build_update(&update).await; // Start the SSG build if we need to builder.new_build_update(&update, &devserver).await; // And then open the app if it's ready match update { BuilderUpdate::Progress { stage: BuildStage::Failed, } => { if exit_on_error { bail!("Build failed for platform: {bundle_format}"); } } BuilderUpdate::Progress { stage: BuildStage::Aborted, } => { if exit_on_error { bail!("Build aborted for platform: {bundle_format}"); } } BuilderUpdate::Progress { .. } => {} BuilderUpdate::CompilerMessage { message } => { screen.push_cargo_log(message); } BuilderUpdate::BuildFailed { err } => { tracing::error!( "{ERROR_STYLE}Build failed{ERROR_STYLE:#}: {}", crate::error::log_stacktrace(&err, 15), ERROR_STYLE = crate::styles::ERROR_STYLE, ); if exit_on_error { return Err(err); } } BuilderUpdate::BuildReady { bundle } => match bundle.mode { BuildMode::Thin { ref cache, .. } => { let elapsed = bundle.time_end.duration_since(bundle.time_start).unwrap(); match builder.hotpatch(&bundle, id, cache).await { Ok(jumptable) => { let pid = match id { BuildId::CLIENT => builder.client.pid, _ => builder.server.as_ref().and_then(|s| s.pid), }; devserver.send_patch(jumptable, elapsed, id, pid).await } Err(err) => { tracing::error!("Failed to hot-patch app: {err}"); if let Some(_patching) = err.downcast_ref::() { tracing::info!("Starting full rebuild: {err}"); builder.full_rebuild().await; devserver.send_reload_start().await; devserver.start_build().await; } } } } BuildMode::Base { .. } | BuildMode::Fat => { _ = builder .open(&bundle, &mut devserver) .await .inspect_err(|e| tracing::error!("Failed to open app: {}", e)); } }, BuilderUpdate::StdoutReceived { msg } => { screen.push_stdio(bundle_format, msg, tracing::Level::INFO); } BuilderUpdate::StderrReceived { msg } => { screen.push_stdio(bundle_format, msg, tracing::Level::ERROR); } BuilderUpdate::ProcessExited { status } => { if status.success() { tracing::info!( r#"Application [{bundle_format}] exited gracefully. • To restart the app, press `r` to rebuild or `o` to open • To exit the server, press `ctrl+c`"# ); } else { tracing::error!( "Application [{bundle_format}] exited with error: {status}" ); if exit_on_error { bail!("Application [{bundle_format}] exited with error: {status}"); } } } BuilderUpdate::ProcessWaitFailed { err } => { tracing::warn!( "Failed to wait for process - maybe it's hung or being debugged?: {err}" ); if exit_on_error { return Err(err.into()); } } } } ServeUpdate::TracingLog { log } => { screen.push_log(log); } ServeUpdate::OpenApp => match builder.use_hotpatch_engine { true if !matches!(builder.client.build.bundle, BundleFormat::Web) => { tracing::warn!( "Opening a native app with hotpatching enabled requires a full rebuild..." ); builder.full_rebuild().await; devserver.send_reload_start().await; devserver.start_build().await; } _ => { if let Err(err) = builder.open_all(&devserver, true).await { tracing::error!( "Failed to open app: {}", crate::error::log_stacktrace(&err, 15) ) } } }, ServeUpdate::Redraw => { // simply returning will cause a redraw } ServeUpdate::ToggleShouldRebuild => { use crate::styles::{ERROR, NOTE_STYLE}; builder.automatic_rebuilds = !builder.automatic_rebuilds; tracing::info!( "Automatic rebuilds are currently: {}", if builder.automatic_rebuilds { format!("{NOTE_STYLE}enabled{NOTE_STYLE:#}") } else { format!("{ERROR}disabled{ERROR:#}") } ) } ServeUpdate::OpenDebugger { id } => { builder.open_debugger(&devserver, id).await; } ServeUpdate::Exit { error } => { _ = builder.shutdown().await; _ = devserver.shutdown().await; match error { Some(err) => return Err(err), None => return Ok(()), } } } } }