diff --git a/packages/cli-config/src/config.rs b/packages/cli-config/src/config.rs index c67ff4473..5ef83a630 100644 --- a/packages/cli-config/src/config.rs +++ b/packages/cli-config/src/config.rs @@ -30,6 +30,11 @@ pub enum Platform { #[cfg_attr(feature = "cli", clap(name = "static-generation"))] #[serde(rename = "static-generation")] StaticGeneration, + + /// Targeting the static generation platform using SSR and Dioxus-Fullstack + #[cfg_attr(feature = "cli", clap(name = "liveview"))] + #[serde(rename = "liveview")] + Liveview, } /// An error that occurs when a platform is not recognized @@ -50,6 +55,7 @@ impl FromStr for Platform { "desktop" => Ok(Self::Desktop), "fullstack" => Ok(Self::Fullstack), "static-generation" => Ok(Self::StaticGeneration), + "liveview" => Ok(Self::Liveview), _ => Err(UnknownPlatformError), } } @@ -78,6 +84,7 @@ impl Platform { Platform::Desktop => "desktop", Platform::Fullstack => "fullstack", Platform::StaticGeneration => "static-generation", + Platform::Liveview => "liveview", } } } diff --git a/packages/cli/src/assets.rs b/packages/cli/src/assets.rs index 02126b5f6..5848e6989 100644 --- a/packages/cli/src/assets.rs +++ b/packages/cli/src/assets.rs @@ -1,14 +1,16 @@ use crate::builder::{ - BuildMessage, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage, + BuildMessage, BuildRequest, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage, }; -use crate::dioxus_crate::DioxusCrate; use crate::Result; use anyhow::Context; use brotli::enc::BrotliEncoderParams; use futures_channel::mpsc::UnboundedSender; use manganis_cli_support::{process_file, AssetManifest, AssetManifestExt, AssetType}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use std::fs; use std::path::Path; +use std::sync::atomic::AtomicUsize; +use std::sync::Arc; use std::{ffi::OsString, path::PathBuf}; use std::{fs::File, io::Write}; use tracing::Level; @@ -17,8 +19,8 @@ use walkdir::WalkDir; /// The temp file name for passing manganis json from linker to current exec. pub const MG_JSON_OUT: &str = "mg-out"; -pub fn asset_manifest(config: &DioxusCrate) -> AssetManifest { - let file_path = config.out_dir().join(MG_JSON_OUT); +pub fn asset_manifest(build: &BuildRequest) -> AssetManifest { + let file_path = build.target_out_dir().join(MG_JSON_OUT); let read = fs::read_to_string(&file_path).unwrap(); _ = fs::remove_file(file_path); let json: Vec = serde_json::from_str(&read).unwrap(); @@ -27,58 +29,64 @@ pub fn asset_manifest(config: &DioxusCrate) -> AssetManifest { } /// Create a head file that contains all of the imports for assets that the user project uses -pub fn create_assets_head(config: &DioxusCrate, manifest: &AssetManifest) -> Result<()> { - let mut file = File::create(config.out_dir().join("__assets_head.html"))?; +pub fn create_assets_head(build: &BuildRequest, manifest: &AssetManifest) -> Result<()> { + let out_dir = build.target_out_dir(); + std::fs::create_dir_all(&out_dir)?; + let mut file = File::create(out_dir.join("__assets_head.html"))?; file.write_all(manifest.head().as_bytes())?; Ok(()) } /// Process any assets collected from the binary pub(crate) fn process_assets( - config: &DioxusCrate, + build: &BuildRequest, manifest: &AssetManifest, progress: &mut UnboundedSender, ) -> anyhow::Result<()> { - let static_asset_output_dir = config.out_dir(); + let static_asset_output_dir = build.target_out_dir(); std::fs::create_dir_all(&static_asset_output_dir) .context("Failed to create static asset output directory")?; - let mut assets_finished: usize = 0; + let assets_finished = Arc::new(AtomicUsize::new(0)); let assets = manifest.assets(); let asset_count = assets.len(); - assets.iter().try_for_each(move |asset| { - if let AssetType::File(file_asset) = asset { - match process_file(file_asset, &static_asset_output_dir) { - Ok(_) => { - // Update the progress - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::OptimizingAssets, - update: UpdateStage::AddMessage(BuildMessage { - level: Level::INFO, - message: MessageType::Text(format!( - "Optimized static asset {}", - file_asset - )), - source: MessageSource::Build, - }), - }); - assets_finished += 1; - _ = progress.start_send(UpdateBuildProgress { - stage: Stage::OptimizingAssets, - update: UpdateStage::SetProgress( - assets_finished as f64 / asset_count as f64, - ), - }); - } - Err(err) => { - tracing::error!("Failed to copy static asset: {}", err); - return Err(err); + assets.par_iter().try_for_each_init( + || progress.clone(), + move |progress, asset| { + if let AssetType::File(file_asset) = asset { + match process_file(file_asset, &static_asset_output_dir) { + Ok(_) => { + // Update the progress + _ = progress.start_send(UpdateBuildProgress { + stage: Stage::OptimizingAssets, + update: UpdateStage::AddMessage(BuildMessage { + level: Level::INFO, + message: MessageType::Text(format!( + "Optimized static asset {}", + file_asset + )), + source: MessageSource::Build, + }), + }); + let assets_finished = + assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + _ = progress.start_send(UpdateBuildProgress { + stage: Stage::OptimizingAssets, + update: UpdateStage::SetProgress( + assets_finished as f64 / asset_count as f64, + ), + }); + } + Err(err) => { + tracing::error!("Failed to copy static asset: {}", err); + return Err(err); + } } } - } - Ok::<(), anyhow::Error>(()) - })?; + Ok::<(), anyhow::Error>(()) + }, + )?; Ok(()) } diff --git a/packages/cli/src/builder/cargo.rs b/packages/cli/src/builder/cargo.rs index 154720725..9bafa456f 100644 --- a/packages/cli/src/builder/cargo.rs +++ b/packages/cli/src/builder/cargo.rs @@ -1,6 +1,7 @@ use super::web::install_web_build_tooling; use super::BuildRequest; use super::BuildResult; +use super::TargetPlatform; use crate::assets::copy_dir_to; use crate::assets::create_assets_head; use crate::assets::{asset_manifest, process_assets, AssetConfigDropGuard}; @@ -12,9 +13,12 @@ use crate::builder::progress::UpdateStage; use crate::link::LinkCommand; use crate::Result; use anyhow::Context; +use dioxus_cli_config::Platform; use futures_channel::mpsc::UnboundedSender; +use manganis_cli_support::AssetManifest; use manganis_cli_support::ManganisSupportGuard; use std::fs::create_dir_all; +use std::path::PathBuf; impl BuildRequest { /// Create a list of arguments for cargo builds @@ -41,11 +45,10 @@ impl BuildRequest { cargo_args.push(features_str); } - if let Some(target) = self.web.then_some("wasm32-unknown-unknown").or(self - .build_arguments - .target_args - .target - .as_deref()) + if let Some(target) = self + .targeting_web() + .then_some("wasm32-unknown-unknown") + .or(self.build_arguments.target_args.target.as_deref()) { cargo_args.push("--target".to_string()); cargo_args.push(target.to_string()); @@ -94,7 +97,7 @@ impl BuildRequest { Ok((cmd, cargo_args)) } - pub async fn build( + pub(crate) async fn build( &self, mut progress: UnboundedSender, ) -> Result { @@ -115,7 +118,7 @@ impl BuildRequest { AssetConfigDropGuard::new(self.dioxus_crate.dioxus_config.web.app.base_path.as_deref()); // If this is a web, build make sure we have the web build tooling set up - if self.web { + if self.targeting_web() { install_web_build_tooling(&mut progress).await?; } @@ -133,13 +136,8 @@ impl BuildRequest { .context("Failed to post process build")?; tracing::info!( - "🚩 Build completed: [./{}]", - self.dioxus_crate - .dioxus_config - .application - .out_dir - .clone() - .display() + "🚩 Build completed: [{}]", + self.dioxus_crate.out_dir().display() ); _ = progress.start_send(UpdateBuildProgress { @@ -161,30 +159,17 @@ impl BuildRequest { update: UpdateStage::Start, }); - // Start Manganis linker intercept. - let linker_args = vec![format!("{}", self.dioxus_crate.out_dir().display())]; - - // Don't block the main thread - manganis should not be running its own std process but it's - // fine to wrap it here at the top - tokio::task::spawn_blocking(move || { - manganis_cli_support::start_linker_intercept( - &LinkCommand::command_name(), - cargo_args, - Some(linker_args), - ) - }) - .await - .unwrap()?; + let assets = self.collect_assets(cargo_args, progress).await?; let file_name = self.dioxus_crate.executable_name(); // Move the final output executable into the dist folder - let out_dir = self.dioxus_crate.out_dir(); + let out_dir = self.target_out_dir(); if !out_dir.is_dir() { create_dir_all(&out_dir)?; } let mut output_path = out_dir.join(file_name); - if self.web { + if self.targeting_web() { output_path.set_extension("wasm"); } else if cfg!(windows) { output_path.set_extension("exe"); @@ -195,37 +180,14 @@ impl BuildRequest { self.copy_assets_dir()?; - let assets = if !self.build_arguments.skip_assets { - let assets = asset_manifest(&self.dioxus_crate); - let dioxus_crate = self.dioxus_crate.clone(); - let mut progress = progress.clone(); - tokio::task::spawn_blocking( - move || -> Result> { - // Collect assets - process_assets(&dioxus_crate, &assets, &mut progress)?; - // Create the __assets_head.html file for bundling - create_assets_head(&dioxus_crate, &assets)?; - Ok(Some(assets)) - }, - ) - .await - .unwrap()? - } else { - None - }; - // Create the build result let build_result = BuildResult { executable: output_path, - web: self.web, - platform: self - .build_arguments - .platform - .expect("To be resolved by now"), + target_platform: self.target_platform, }; // If this is a web build, run web post processing steps - if self.web { + if self.targeting_web() { self.post_process_web_build(&build_result, assets.as_ref(), progress) .await?; } @@ -233,6 +195,45 @@ impl BuildRequest { Ok(build_result) } + async fn collect_assets( + &self, + cargo_args: Vec, + progress: &mut UnboundedSender, + ) -> anyhow::Result> { + // If this is the server build, the client build already copied any assets we need + if self.target_platform == TargetPlatform::Server { + return Ok(None); + } + // If assets are skipped, we don't need to collect them + if self.build_arguments.skip_assets { + return Ok(None); + } + + // Start Manganis linker intercept. + let linker_args = vec![format!("{}", self.target_out_dir().display())]; + + // Don't block the main thread - manganis should not be running its own std process but it's + // fine to wrap it here at the top + let build = self.clone(); + let mut progress = progress.clone(); + tokio::task::spawn_blocking(move || { + manganis_cli_support::start_linker_intercept( + &LinkCommand::command_name(), + cargo_args, + Some(linker_args), + )?; + let assets = asset_manifest(&build); + // Collect assets from the asset manifest the linker intercept created + process_assets(&build, &assets, &mut progress)?; + // Create the __assets_head.html file for bundling + create_assets_head(&build, &assets)?; + + Ok(Some(assets)) + }) + .await + .unwrap() + } + pub fn copy_assets_dir(&self) -> anyhow::Result<()> { tracing::info!("Copying public assets to the output directory..."); let out_dir = self.dioxus_crate.out_dir(); @@ -240,7 +241,7 @@ impl BuildRequest { if asset_dir.is_dir() { // Only pre-compress the assets from the web build. Desktop assets are not served, so they don't need to be pre_compressed - let pre_compress = self.web + let pre_compress = self.targeting_web() && self .dioxus_crate .should_pre_compress_web_assets(self.build_arguments.release); @@ -249,4 +250,16 @@ impl BuildRequest { } Ok(()) } + + /// Get the output directory for a specific built target + pub fn target_out_dir(&self) -> PathBuf { + let out_dir = self.dioxus_crate.out_dir(); + match self.build_arguments.platform { + Some(Platform::Fullstack | Platform::StaticGeneration) => match self.target_platform { + TargetPlatform::Web => out_dir.join("public"), + _ => out_dir, + }, + _ => out_dir, + } + } } diff --git a/packages/cli/src/builder/fullstack.rs b/packages/cli/src/builder/fullstack.rs index e39939429..cc56dc80f 100644 --- a/packages/cli/src/builder/fullstack.rs +++ b/packages/cli/src/builder/fullstack.rs @@ -1,10 +1,11 @@ use crate::builder::Build; use crate::dioxus_crate::DioxusCrate; -use dioxus_cli_config::Platform; use crate::builder::BuildRequest; use std::path::PathBuf; +use super::TargetPlatform; + static CLIENT_RUST_FLAGS: &[&str] = &["-Cdebuginfo=none", "-Cstrip=debuginfo"]; // The `opt-level=2` increases build times, but can noticeably decrease time // between saving changes and being able to interact with an app. The "overall" @@ -56,15 +57,10 @@ impl BuildRequest { target_directory: PathBuf, rust_flags: &[&str], feature: String, - web: bool, + target_platform: TargetPlatform, ) -> Self { let config = config.clone(); let mut build = build.clone(); - build.platform = Some(if web { - Platform::Web - } else { - Platform::Desktop - }); // Set the target directory we are building the server in let target_dir = get_target_directory(&build, target_directory); // Add the server feature to the features we pass to the build @@ -74,12 +70,12 @@ impl BuildRequest { let rust_flags = fullstack_rust_flags(&build, rust_flags); Self { - web, serve, build_arguments: build.clone(), dioxus_crate: config, rust_flags, target_dir, + target_platform, } } @@ -91,7 +87,7 @@ impl BuildRequest { config.server_target_dir(), SERVER_RUST_FLAGS, build.target_args.server_feature.clone(), - false, + TargetPlatform::Server, ) } @@ -103,7 +99,7 @@ impl BuildRequest { config.client_target_dir(), CLIENT_RUST_FLAGS, build.target_args.client_feature.clone(), - true, + TargetPlatform::Web, ) } } diff --git a/packages/cli/src/builder/mod.rs b/packages/cli/src/builder/mod.rs index e855fb4c5..c6c84f67b 100644 --- a/packages/cli/src/builder/mod.rs +++ b/packages/cli/src/builder/mod.rs @@ -18,14 +18,37 @@ pub use progress::{ BuildMessage, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage, }; +/// The target platform for the build +/// This is very similar to the Platform enum, but we need to be able to differentiate between the +/// server and web targets for the fullstack platform +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TargetPlatform { + Web, + Desktop, + Server, + Liveview, +} + +impl std::fmt::Display for TargetPlatform { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TargetPlatform::Web => write!(f, "web"), + TargetPlatform::Desktop => write!(f, "desktop"), + TargetPlatform::Server => write!(f, "server"), + TargetPlatform::Liveview => write!(f, "liveview"), + } + } +} + /// A request for a project to be built +#[derive(Clone)] pub struct BuildRequest { /// Whether the build is for serving the application pub serve: bool, - /// Whether this is a web build - pub web: bool, /// The configuration for the crate we are building pub dioxus_crate: DioxusCrate, + /// The target platform for the build + pub target_platform: TargetPlatform, /// The arguments for the build pub build_arguments: Build, /// The rustc flags to pass to the build @@ -41,28 +64,32 @@ impl BuildRequest { build_arguments: impl Into, ) -> Vec { let build_arguments = build_arguments.into(); - let dioxus_crate = dioxus_crate.clone(); let platform = build_arguments.platform(); + let single_platform = |platform| { + let dioxus_crate = dioxus_crate.clone(); + vec![Self { + serve, + dioxus_crate, + build_arguments: build_arguments.clone(), + target_platform: platform, + rust_flags: Default::default(), + target_dir: Default::default(), + }] + }; match platform { - Platform::Web | Platform::Desktop => { - let web = platform == Platform::Web; - vec![Self { - serve, - web, - dioxus_crate, - build_arguments, - rust_flags: Default::default(), - target_dir: Default::default(), - }] - } + Platform::Web => single_platform(TargetPlatform::Web), + Platform::Liveview => single_platform(TargetPlatform::Liveview), + Platform::Desktop => single_platform(TargetPlatform::Desktop), Platform::StaticGeneration | Platform::Fullstack => { - Self::new_fullstack(dioxus_crate, build_arguments, serve) + Self::new_fullstack(dioxus_crate.clone(), build_arguments, serve) } _ => unimplemented!("Unknown platform: {platform:?}"), } } - pub async fn build_all_parallel(build_requests: Vec) -> Result> { + pub(crate) async fn build_all_parallel( + build_requests: Vec, + ) -> Result> { let multi_platform_build = build_requests.len() > 1; let mut build_progress = Vec::new(); let mut set = tokio::task::JoinSet::new(); @@ -104,13 +131,17 @@ impl BuildRequest { Ok(all_results) } + + /// Check if the build is targeting the web platform + pub fn targeting_web(&self) -> bool { + self.target_platform == TargetPlatform::Web + } } #[derive(Debug, Clone)] pub(crate) struct BuildResult { pub executable: PathBuf, - pub web: bool, - pub platform: Platform, + pub target_platform: TargetPlatform, } impl BuildResult { @@ -121,24 +152,26 @@ impl BuildResult { fullstack_address: Option, workspace: &std::path::Path, ) -> std::io::Result> { - if self.web { + if self.target_platform == TargetPlatform::Web { return Ok(None); } + if self.target_platform == TargetPlatform::Server { + tracing::trace!("Proxying fullstack server from port {fullstack_address:?}"); + } let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address); let executable = self.executable.canonicalize()?; - Ok(Some( - Command::new(executable) - // When building the fullstack server, we need to forward the serve arguments (like port) to the fullstack server through env vars - .env( - dioxus_cli_config::__private::SERVE_ENV, - serde_json::to_string(&arguments).unwrap(), - ) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .kill_on_drop(true) - .current_dir(workspace) - .spawn()?, - )) + let mut cmd = Command::new(executable); + cmd + // When building the fullstack server, we need to forward the serve arguments (like port) to the fullstack server through env vars + .env( + dioxus_cli_config::__private::SERVE_ENV, + serde_json::to_string(&arguments).unwrap(), + ) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .current_dir(workspace); + Ok(Some(cmd.spawn()?)) } } diff --git a/packages/cli/src/builder/web.rs b/packages/cli/src/builder/web.rs index b8fed77fe..bb3f7e5e7 100644 --- a/packages/cli/src/builder/web.rs +++ b/packages/cli/src/builder/web.rs @@ -124,7 +124,7 @@ impl BuildRequest { let input_path = output_location.with_extension("wasm"); // Create the directory where the bindgen output will be placed - let bindgen_outdir = self.dioxus_crate.out_dir().join("assets").join("dioxus"); + let bindgen_outdir = self.target_out_dir().join("assets").join("dioxus"); // Run wasm-bindgen self.run_wasm_bindgen(&input_path, &bindgen_outdir).await?; @@ -183,7 +183,7 @@ impl BuildRequest { // If we do this too early, the wasm won't be ready but the index.html will be served, leading // to test failures and broken pages. let html = self.prepare_html(assets, progress)?; - let html_path = self.dioxus_crate.out_dir().join("index.html"); + let html_path = self.target_out_dir().join("index.html"); std::fs::write(html_path, html)?; Ok(()) diff --git a/packages/cli/src/serve/builder.rs b/packages/cli/src/serve/builder.rs index 3da60cf05..c89488094 100644 --- a/packages/cli/src/serve/builder.rs +++ b/packages/cli/src/serve/builder.rs @@ -1,14 +1,15 @@ use crate::builder::BuildRequest; use crate::builder::BuildResult; +use crate::builder::TargetPlatform; use crate::builder::UpdateBuildProgress; use crate::dioxus_crate::DioxusCrate; +use crate::serve::next_or_pending; use crate::serve::Serve; use crate::Result; -use dioxus_cli_config::Platform; use futures_channel::mpsc::UnboundedReceiver; +use futures_util::future::OptionFuture; use futures_util::stream::select_all; use futures_util::StreamExt; -use futures_util::{future::OptionFuture, stream::FuturesUnordered}; use std::process::Stdio; use tokio::{ process::{Child, Command}, @@ -21,7 +22,7 @@ pub struct Builder { build_results: Option>>>, /// The progress of the builds - build_progress: Vec<(Platform, UnboundedReceiver)>, + build_progress: Vec<(TargetPlatform, UnboundedReceiver)>, /// The application we are building config: DioxusCrate, @@ -30,7 +31,7 @@ pub struct Builder { serve: Serve, /// The children of the build process - pub children: Vec<(Platform, Child)>, + pub children: Vec<(TargetPlatform, Child)>, } impl Builder { @@ -58,7 +59,7 @@ impl Builder { for build_request in build_requests { let (mut tx, rx) = futures_channel::mpsc::unbounded(); self.build_progress - .push((build_request.build_arguments.platform(), rx)); + .push((build_request.target_platform, rx)); set.spawn(async move { let res = build_request.build(tx.clone()).await; @@ -94,24 +95,28 @@ impl Builder { .iter_mut() .map(|(platform, rx)| rx.map(move |update| (*platform, update))), ); + let next = next_or_pending(next.next()); // The ongoing builds directly let results: OptionFuture<_> = self.build_results.as_mut().into(); + let results = next_or_pending(results); // The process exits - let mut process_exited = self + let children_empty = self.children.is_empty(); + let process_exited = self .children .iter_mut() - .map(|(_, child)| async move { - let status = child.wait().await.ok(); - - BuilderUpdate::ProcessExited { status } - }) - .collect::>(); + .map(|(target, child)| Box::pin(async move { (*target, child.wait().await) })); + let process_exited = async move { + if children_empty { + return futures_util::future::pending().await; + } + futures_util::future::select_all(process_exited).await + }; // Wait for the next build result tokio::select! { - Some(build_results) = results => { + build_results = results => { self.build_results = None; // If we have a build result, bubble it up to the main loop @@ -119,17 +124,13 @@ impl Builder { Ok(BuilderUpdate::Ready { results: build_results }) } - Some((platform, update)) = next.next() => { + (platform, update) = next => { // If we have a build progress, send it to the screen - Ok(BuilderUpdate::Progress { platform, update }) + Ok(BuilderUpdate::Progress { platform, update }) } - Some(exit_status) = process_exited.next() => { - Ok(exit_status) + ((target, exit_status), _, _) = process_exited => { + Ok(BuilderUpdate::ProcessExited { status: exit_status, target_platform: target }) } - else => { - std::future::pending::<()>().await; - unreachable!("Pending cannot resolve") - }, } } @@ -173,13 +174,14 @@ impl Builder { pub enum BuilderUpdate { Progress { - platform: Platform, + platform: TargetPlatform, update: UpdateBuildProgress, }, Ready { results: Vec, }, ProcessExited { - status: Option, + target_platform: TargetPlatform, + status: Result, }, } diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index 357f944aa..1e1505090 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -1,9 +1,12 @@ -use crate::builder::{Stage, UpdateBuildProgress, UpdateStage}; +use std::future::{poll_fn, Future, IntoFuture}; +use std::task::Poll; + +use crate::builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage}; use crate::cli::serve::Serve; use crate::dioxus_crate::DioxusCrate; use crate::tracer::CLILogControl; use crate::Result; -use dioxus_cli_config::Platform; +use futures_util::FutureExt; use tokio::task::yield_now; mod builder; @@ -103,7 +106,7 @@ pub async fn serve_all( // Run the server in the background // Waiting for updates here lets us tap into when clients are added/removed if let Some(msg) = msg { - screen.new_ws_message(Platform::Web, msg); + screen.new_ws_message(TargetPlatform::Web, msg); } } @@ -135,8 +138,11 @@ pub async fn serve_all( for build_result in results.iter() { let child = build_result.open(&serve.server_arguments, server.fullstack_address(), &dioxus_crate.workspace_dir()); match child { - Ok(Some(child_proc)) => builder.children.push((build_result.platform,child_proc)), - Err(_e) => break, + Ok(Some(child_proc)) => builder.children.push((build_result.target_platform, child_proc)), + Err(e) => { + tracing::error!("Failed to open build result: {e}"); + break; + }, _ => {} } } @@ -150,10 +156,20 @@ pub async fn serve_all( }, // If the process exited *cleanly*, we can exit - Ok(BuilderUpdate::ProcessExited { status, ..}) => { - if let Some(status) = status { - if status.success() { - break; + Ok(BuilderUpdate::ProcessExited { status, target_platform }) => { + // Then remove the child process + builder.children.retain(|(platform, _)| *platform != target_platform); + match status { + Ok(status) => { + if status.success() { + break; + } + else { + tracing::error!("Application exited with status: {status}"); + } + }, + Err(e) => { + tracing::error!("Application exited with error: {e}"); } } } @@ -187,3 +203,20 @@ pub async fn serve_all( Ok(()) } + +// Grab the output of a future that returns an option or wait forever +pub(crate) fn next_or_pending(f: F) -> impl Future +where + F: IntoFuture>, +{ + let pinned = f.into_future().fuse(); + let mut pinned = Box::pin(pinned); + poll_fn(move |cx| { + let next = pinned.as_mut().poll(cx); + match next { + Poll::Ready(Some(next)) => Poll::Ready(next), + _ => Poll::Pending, + } + }) + .fuse() +} diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index bac704a53..19e3197a2 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -1,6 +1,9 @@ use crate::{ - builder::{BuildMessage, MessageSource, MessageType, Stage, UpdateBuildProgress}, + builder::{ + BuildMessage, MessageSource, MessageType, Stage, TargetPlatform, UpdateBuildProgress, + }, dioxus_crate::DioxusCrate, + serve::next_or_pending, tracer::CLILogControl, }; use crate::{ @@ -16,13 +19,13 @@ use crossterm::{ }; use dioxus_cli_config::{AddressArguments, Platform}; use dioxus_hot_reload::ClientMsg; -use futures_util::{future::select_all, Future, StreamExt}; +use futures_util::{future::select_all, Future, FutureExt, StreamExt}; use ratatui::{prelude::*, widgets::*, TerminalOptions, Viewport}; use std::{ cell::RefCell, collections::{HashMap, HashSet}, + fmt::Display, io::{self, stdout}, - pin::Pin, rc::Rc, sync::atomic::Ordering, time::{Duration, Instant}, @@ -35,9 +38,30 @@ use tracing::Level; use super::{Builder, Server, Watcher}; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum LogSource { + Internal, + Target(TargetPlatform), +} + +impl Display for LogSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LogSource::Internal => write!(f, "CLI"), + LogSource::Target(platform) => write!(f, "{platform}"), + } + } +} + +impl From for LogSource { + fn from(platform: TargetPlatform) -> Self { + LogSource::Target(platform) + } +} + #[derive(Default)] pub struct BuildProgress { - build_logs: HashMap, + build_logs: HashMap, } impl BuildProgress { @@ -67,7 +91,7 @@ pub struct Output { _dx_version: String, interactive: bool, pub(crate) build_progress: BuildProgress, - running_apps: HashMap, + running_apps: HashMap, is_cli_release: bool, platform: Platform, @@ -162,34 +186,87 @@ impl Output { }) } + /// Add a message from stderr to the logs + fn push_stderr(&mut self, platform: TargetPlatform, stderr: String) { + self.set_tab(Tab::BuildLog); + let source = platform.into(); + + self.running_apps + .get_mut(&platform) + .unwrap() + .output + .as_mut() + .unwrap() + .stderr_line + .push_str(&stderr); + self.build_progress + .build_logs + .get_mut(&source) + .unwrap() + .messages + .push(BuildMessage { + level: Level::ERROR, + message: MessageType::Text(stderr), + source: MessageSource::App, + }); + } + + /// Add a message from stdout to the logs + fn push_stdout(&mut self, platform: TargetPlatform, stdout: String) { + let source = platform.into(); + + self.running_apps + .get_mut(&platform) + .unwrap() + .output + .as_mut() + .unwrap() + .stdout_line + .push_str(&stdout); + self.build_progress + .build_logs + .get_mut(&source) + .unwrap() + .messages + .push(BuildMessage { + level: Level::INFO, + message: MessageType::Text(stdout), + source: MessageSource::App, + }); + } + /// Wait for either the ctrl_c handler or the next event /// /// Why is the ctrl_c handler here? /// /// Also tick animations every few ms pub async fn wait(&mut self) -> io::Result { - // sorry lord - let user_input = match self.events.as_mut() { - Some(events) => { - let pinned: Pin>>>> = - Box::pin(events.next()); - pinned - } - None => Box::pin(futures_util::future::pending()) as Pin>>, + fn ok_and_some(f: F) -> impl Future + where + F: Future, E>>, + { + next_or_pending(async move { f.await.ok().flatten() }) + } + let user_input = async { + let events = self.events.as_mut()?; + events.next().await }; + let user_input = ok_and_some(user_input.map(|e| e.transpose())); let has_running_apps = !self.running_apps.is_empty(); let next_stdout = self.running_apps.values_mut().map(|app| { let future = async move { let (stdout, stderr) = match &mut app.output { - Some(out) => (out.stdout.next_line(), out.stderr.next_line()), + Some(out) => ( + ok_and_some(out.stdout.next_line()), + ok_and_some(out.stderr.next_line()), + ), None => return futures_util::future::pending().await, }; tokio::select! { - Ok(Some(line)) = stdout => (app.result.platform, Some(line), None), - Ok(Some(line)) = stderr => (app.result.platform, None, Some(line)), - else => futures_util::future::pending().await, + line = stdout => (app.result.target_platform, Some(line), None), + line = stderr => (app.result.target_platform, None, Some(line)), } }; Box::pin(future) @@ -203,34 +280,22 @@ impl Output { } }; - let animation_timeout = tokio::time::sleep(Duration::from_millis(300)); let tui_log_rx = &mut self.log_control.tui_rx; + let next_tui_log = next_or_pending(tui_log_rx.next()); tokio::select! { (platform, stdout, stderr) = next_stdout => { if let Some(stdout) = stdout { - self.running_apps.get_mut(&platform).unwrap().output.as_mut().unwrap().stdout_line.push_str(&stdout); - self.push_log(platform, BuildMessage { - level: Level::INFO, - message: MessageType::Text(stdout), - source: MessageSource::App, - }) + self.push_stdout(platform, stdout); } if let Some(stderr) = stderr { - self.set_tab(Tab::BuildLog); - - self.running_apps.get_mut(&platform).unwrap().output.as_mut().unwrap().stderr_line.push_str(&stderr); - self.build_progress.build_logs.get_mut(&platform).unwrap().messages.push(BuildMessage { - level: Level::ERROR, - message: MessageType::Text(stderr), - source: MessageSource::App, - }); + self.push_stderr(platform, stderr); } }, // Handle internal CLI tracing logs. - Some(log) = tui_log_rx.next() => { - self.push_log(self.platform, BuildMessage { + log = next_tui_log => { + self.push_log(LogSource::Internal, BuildMessage { level: Level::INFO, message: MessageType::Text(log), source: MessageSource::Dev, @@ -238,13 +303,10 @@ impl Output { } event = user_input => { - if self.handle_events(event.unwrap().unwrap()).await? { + if self.handle_events(event).await? { return Ok(true) } - // self.handle_input(event.unwrap().unwrap())?; } - - _ = animation_timeout => {} } Ok(false) @@ -330,16 +392,13 @@ impl Output { } Event::Key(key) if key.code == KeyCode::Char('c') => { // Clear the currently selected build logs. - let build = self - .build_progress - .build_logs - .get_mut(&self.platform) - .unwrap(); - let msgs = match self.tab { - Tab::Console => &mut build.stdout_logs, - Tab::BuildLog => &mut build.messages, - }; - msgs.clear(); + for build in self.build_progress.build_logs.values_mut() { + let msgs = match self.tab { + Tab::Console => &mut build.stdout_logs, + Tab::BuildLog => &mut build.messages, + }; + msgs.clear(); + } } Event::Key(key) if key.code == KeyCode::Char('1') => self.set_tab(Tab::Console), Event::Key(key) if key.code == KeyCode::Char('2') => self.set_tab(Tab::BuildLog), @@ -362,7 +421,11 @@ impl Output { Ok(false) } - pub fn new_ws_message(&mut self, platform: Platform, message: axum::extract::ws::Message) { + pub fn new_ws_message( + &mut self, + platform: TargetPlatform, + message: axum::extract::ws::Message, + ) { if let axum::extract::ws::Message::Text(text) = message { let msg = serde_json::from_str::(text.as_str()); match msg { @@ -402,7 +465,7 @@ impl Output { // todo: re-enable #[allow(unused)] - fn is_snapped(&self, _platform: Platform) -> bool { + fn is_snapped(&self, _platform: LogSource) -> bool { true // let prev_scrol = self // .num_lines_with_wrapping @@ -414,12 +477,13 @@ impl Output { self.scroll = (self.num_lines_with_wrapping).saturating_sub(self.term_height); } - pub fn push_log(&mut self, platform: Platform, message: BuildMessage) { - let snapped = self.is_snapped(platform); + pub fn push_log(&mut self, platform: impl Into, message: BuildMessage) { + let source = platform.into(); + let snapped = self.is_snapped(source); self.build_progress .build_logs - .entry(platform) + .entry(source) .or_default() .stdout_logs .push(message); @@ -429,8 +493,9 @@ impl Output { } } - pub fn new_build_logs(&mut self, platform: Platform, update: UpdateBuildProgress) { - let snapped = self.is_snapped(platform); + pub fn new_build_logs(&mut self, platform: impl Into, update: UpdateBuildProgress) { + let source = platform.into(); + let snapped = self.is_snapped(source); // when the build is finished, switch to the console if update.stage == Stage::Finished { @@ -439,7 +504,7 @@ impl Output { self.build_progress .build_logs - .entry(platform) + .entry(source) .or_default() .update(update); @@ -454,7 +519,7 @@ impl Output { .children .iter_mut() .find_map(|(platform, child)| { - if platform == &result.platform { + if platform == &result.target_platform { let stdout = child.stdout.take().unwrap(); let stderr = child.stderr.take().unwrap(); Some((stdout, stderr)) @@ -463,7 +528,7 @@ impl Output { } }); - let platform = result.platform; + let platform = result.target_platform; let stdout = out.map(|(stdout, stderr)| RunningAppOutput { stdout: BufReader::new(stdout).lines(), @@ -480,7 +545,8 @@ impl Output { self.running_apps.insert(platform, app); // Finish the build progress for the platform that just finished building - if let Some(build) = self.build_progress.build_logs.get_mut(&platform) { + let source = platform.into(); + if let Some(build) = self.build_progress.build_logs.get_mut(&source) { build.stage = Stage::Finished; } } @@ -735,12 +801,17 @@ impl Output { let mut events = vec![event]; // Collect all the events within the next 10ms in one stream - loop { - let next = self.events.as_mut().unwrap().next(); - tokio::select! { - msg = next => events.push(msg.unwrap().unwrap()), - _ = tokio::time::sleep(Duration::from_millis(1)) => break + let collect_events = async { + loop { + let Some(Ok(next)) = self.events.as_mut().unwrap().next().await else { + break; + }; + events.push(next); } + }; + tokio::select! { + _ = collect_events => {}, + _ = tokio::time::sleep(Duration::from_millis(10)) => {} } // Debounce events within the same frame diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index d731cb1b9..ab125c779 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -1,5 +1,5 @@ use crate::dioxus_crate::DioxusCrate; -use crate::serve::Serve; +use crate::serve::{next_or_pending, Serve}; use crate::{Error, Result}; use axum::extract::{Request, State}; use axum::middleware::{self, Next}; @@ -240,6 +240,7 @@ impl Server { .enumerate() .map(|(idx, socket)| async move { (idx, socket.next().await) }) .collect::>(); + let next_new_message = next_or_pending(new_message.next()); tokio::select! { new_hot_reload_socket = &mut new_hot_reload_socket => { @@ -266,7 +267,7 @@ impl Server { panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); } } - Some((idx, message)) = new_message.next() => { + (idx, message) = next_new_message => { match message { Some(Ok(message)) => return Some(message), _ => { diff --git a/packages/fullstack/src/axum_adapter.rs b/packages/fullstack/src/axum_adapter.rs index d6a836a17..38030b4b3 100644 --- a/packages/fullstack/src/axum_adapter.rs +++ b/packages/fullstack/src/axum_adapter.rs @@ -125,7 +125,7 @@ pub trait DioxusRouterExt { /// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); /// let router = axum::Router::new() /// // Server side render the application, serve static assets, and register server functions - /// .serve_static_assets("dist") + /// .serve_static_assets() /// // Server render the application /// // ... /// .into_make_service(); @@ -133,7 +133,7 @@ pub trait DioxusRouterExt { /// axum::serve(listener, router).await.unwrap(); /// } /// ``` - fn serve_static_assets(self, assets_path: impl Into) -> Self + fn serve_static_assets(self) -> Self where Self: Sized; @@ -203,18 +203,16 @@ where self } - // TODO: This is a breaking change, but we should probably serve static assets from a different directory than dist where the server executable is located - // This would prevent issues like https://github.com/DioxusLabs/dioxus/issues/2327 - fn serve_static_assets(mut self, assets_path: impl Into) -> Self { + fn serve_static_assets(mut self) -> Self { use tower_http::services::{ServeDir, ServeFile}; - let assets_path = assets_path.into(); + let public_path = crate::public_path(); - // Serve all files in dist folder except index.html - let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| { + // Serve all files in public folder except index.html + let dir = std::fs::read_dir(&public_path).unwrap_or_else(|e| { panic!( - "Couldn't read assets directory at {:?}: {}", - &assets_path, e + "Couldn't read public directory at {:?}: {}", + &public_path, e ) }); @@ -224,7 +222,7 @@ where continue; } let route = path - .strip_prefix(&assets_path) + .strip_prefix(&public_path) .unwrap() .iter() .map(|segment| { @@ -251,10 +249,7 @@ where let ssr_state = SSRState::new(&cfg); // Add server functions and render index.html - #[allow(unused_mut)] - let mut server = self - .serve_static_assets(cfg.assets_path.clone()) - .register_server_functions(); + let server = self.serve_static_assets().register_server_functions(); server.fallback( get(render_handler).with_state( diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 889c010ca..cbf73dc54 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -155,7 +155,7 @@ async fn launch_server( let cfg = platform_config.server_cfg.build(); - let mut router = router.serve_static_assets(cfg.assets_path.clone()); + let mut router = router.serve_static_assets(); router.fallback( axum::routing::get(crate::axum_adapter::render_handler).with_state( diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index aadc8638d..25a4b70e8 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -11,7 +11,6 @@ pub struct ServeConfigBuilder { pub(crate) root_id: Option<&'static str>, pub(crate) index_html: Option, pub(crate) index_path: Option, - pub(crate) assets_path: Option, pub(crate) incremental: Option, } @@ -22,7 +21,6 @@ impl ServeConfigBuilder { root_id: None, index_html: None, index_path: None, - assets_path: None, incremental: None, } } @@ -51,25 +49,15 @@ impl ServeConfigBuilder { self } - /// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist) - pub fn assets_path(mut self, assets_path: PathBuf) -> Self { - self.assets_path = Some(assets_path); - self - } - /// Build the ServeConfig pub fn build(self) -> ServeConfig { - let assets_path = self.assets_path.unwrap_or( - dioxus_cli_config::CURRENT_CONFIG - .as_ref() - .map(|c| c.application.out_dir.clone()) - .unwrap_or("dist".into()), - ); + // The CLI always bundles static assets into the exe/public directory + let public_path = public_path(); let index_path = self .index_path .map(PathBuf::from) - .unwrap_or_else(|| assets_path.join("index.html")); + .unwrap_or_else(|| public_path.join("index.html")); let root_id = self.root_id.unwrap_or("main"); @@ -81,14 +69,23 @@ impl ServeConfigBuilder { ServeConfig { index, - assets_path, incremental: self.incremental, } } } +/// Get the path to the public assets directory to serve static files from +pub(crate) fn public_path() -> PathBuf { + // The CLI always bundles static assets into the exe/public directory + std::env::current_exe() + .expect("Failed to get current executable path") + .parent() + .unwrap() + .join("public") +} + fn load_index_path(path: PathBuf) -> String { - let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built."); + let mut file = File::open(&path).unwrap_or_else(|_| panic!("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built. Tried to open file at path: {path:?}")); let mut contents = String::new(); file.read_to_string(&mut contents) @@ -156,8 +153,6 @@ pub(crate) struct IndexHtml { #[derive(Clone)] pub struct ServeConfig { pub(crate) index: IndexHtml, - #[allow(unused)] - pub(crate) assets_path: PathBuf, pub(crate) incremental: Option, } diff --git a/packages/static-generation/src/ssg.rs b/packages/static-generation/src/ssg.rs index 20b1af53a..142e64de3 100644 --- a/packages/static-generation/src/ssg.rs +++ b/packages/static-generation/src/ssg.rs @@ -70,6 +70,8 @@ pub async fn generate_static_site( .map(|c| c.application.out_dir.clone()) .unwrap_or("./dist".into()); + let assets_path = assets_path.join("public"); + copy_static_files(&assets_path, &config.output_dir)?; Ok(())