Ver código fonte

Add liveview support to the CLI and make fullstack runnable from dist (#2759)

* add liveview cli support
* Fix TUI fullstack deadlock
* look for fullstack assets in the public directory
* Fix fullstack with the CLI
* Fix static generation server
Evan Almloff 11 meses atrás
pai
commit
e5e578d27b

+ 7 - 0
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",
         }
     }
 }

+ 47 - 39
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<String> = 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<UpdateBuildProgress>,
 ) -> 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(())
 }

+ 69 - 56
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<UpdateBuildProgress>,
     ) -> Result<BuildResult> {
@@ -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<Option<manganis_cli_support::AssetManifest>> {
-                    // 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<String>,
+        progress: &mut UnboundedSender<UpdateBuildProgress>,
+    ) -> anyhow::Result<Option<AssetManifest>> {
+        // 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,
+        }
+    }
 }

+ 6 - 10
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,
         )
     }
 }

+ 65 - 32
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<Build>,
     ) -> Vec<Self> {
         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<BuildRequest>) -> Result<Vec<BuildResult>> {
+    pub(crate) async fn build_all_parallel(
+        build_requests: Vec<BuildRequest>,
+    ) -> Result<Vec<BuildResult>> {
         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<SocketAddr>,
         workspace: &std::path::Path,
     ) -> std::io::Result<Option<Child>> {
-        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()?))
     }
 }

+ 2 - 2
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(())

+ 25 - 23
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<JoinHandle<Result<Vec<BuildResult>>>>,
 
     /// The progress of the builds
-    build_progress: Vec<(Platform, UnboundedReceiver<UpdateBuildProgress>)>,
+    build_progress: Vec<(TargetPlatform, UnboundedReceiver<UpdateBuildProgress>)>,
 
     /// 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::<FuturesUnordered<_>>();
+            .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<BuildResult>,
     },
     ProcessExited {
-        status: Option<std::process::ExitStatus>,
+        target_platform: TargetPlatform,
+        status: Result<std::process::ExitStatus, std::io::Error>,
     },
 }

+ 42 - 9
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, T>(f: F) -> impl Future<Output = T>
+where
+    F: IntoFuture<Output = Option<T>>,
+{
+    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()
+}

+ 135 - 64
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<TargetPlatform> for LogSource {
+    fn from(platform: TargetPlatform) -> Self {
+        LogSource::Target(platform)
+    }
+}
+
 #[derive(Default)]
 pub struct BuildProgress {
-    build_logs: HashMap<Platform, ActiveBuild>,
+    build_logs: HashMap<LogSource, ActiveBuild>,
 }
 
 impl BuildProgress {
@@ -67,7 +91,7 @@ pub struct Output {
     _dx_version: String,
     interactive: bool,
     pub(crate) build_progress: BuildProgress,
-    running_apps: HashMap<Platform, RunningApp>,
+    running_apps: HashMap<TargetPlatform, RunningApp>,
     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<bool> {
-        // sorry lord
-        let user_input = match self.events.as_mut() {
-            Some(events) => {
-                let pinned: Pin<Box<dyn Future<Output = Option<Result<Event, _>>>>> =
-                    Box::pin(events.next());
-                pinned
-            }
-            None => Box::pin(futures_util::future::pending()) as Pin<Box<dyn Future<Output = _>>>,
+        fn ok_and_some<F, T, E>(f: F) -> impl Future<Output = T>
+        where
+            F: Future<Output = Result<Option<T>, 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::<ClientMsg>(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<LogSource>, 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<LogSource>, 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

+ 3 - 2
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::<FuturesUnordered<_>>();
+        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),
                     _ => {

+ 10 - 15
packages/fullstack/src/axum_adapter.rs

@@ -125,7 +125,7 @@ pub trait DioxusRouterExt<S> {
     ///     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<S> {
     ///     axum::serve(listener, router).await.unwrap();
     /// }
     /// ```
-    fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> 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<std::path::PathBuf>) -> 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(

+ 1 - 1
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(

+ 14 - 19
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<String>,
     pub(crate) index_path: Option<PathBuf>,
-    pub(crate) assets_path: Option<PathBuf>,
     pub(crate) incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
 }
 
@@ -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<dioxus_ssr::incremental::IncrementalRendererConfig>,
 }
 

+ 2 - 0
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(())