Переглянути джерело

Forward cli serve settings and make it easier to provide platform config to fullstack (#2521)

* assert that launch never returns for better compiler errors

* fix static generation launch function

* fix web renderer

* pass context providers into server functions

* add an example for FromContext

* clean up DioxusRouterExt

* fix server function context

* fix fullstack desktop example

* forward CLI serve settings to fullstack

* re-export serve config at the root of fullstack

* forward env directly instead of using a guard

* just set the port in the CLI for fullstack playwright tests

* fix fullstack dioxus-cli-config feature

* fix launch server merge conflicts

* fix fullstack launch context

* run web playwright tests on 9999 to avoid port conflicts with other local servers

* fix playwright tests
Evan Almloff 1 рік тому
батько
коміт
ffa36a67c3

+ 11 - 10
Cargo.lock

@@ -1138,7 +1138,7 @@ checksum = "aa7015584550945f11fdfb7af113d30e2727468ec281c1d7f28cc1019196c25d"
 dependencies = [
  "anyhow",
  "auth-git2",
- "clap 4.5.4",
+ "clap 4.5.7",
  "console",
  "dialoguer",
  "env_logger 0.11.3",
@@ -1361,9 +1361,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.4"
+version = "4.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -1371,9 +1371,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.2"
+version = "4.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1383,9 +1383,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.4"
+version = "4.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
+checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
@@ -1730,7 +1730,7 @@ dependencies = [
  "anes",
  "cast",
  "ciborium",
- "clap 4.5.4",
+ "clap 4.5.7",
  "criterion-plot 0.5.0",
  "futures",
  "is-terminal",
@@ -2190,7 +2190,7 @@ dependencies = [
  "cargo_metadata 0.18.1",
  "cargo_toml 0.18.0",
  "chrono",
- "clap 4.5.4",
+ "clap 4.5.7",
  "colored 2.1.0",
  "ctrlc",
  "dioxus-autofmt",
@@ -2252,7 +2252,7 @@ name = "dioxus-cli-config"
 version = "0.5.2"
 dependencies = [
  "cargo_toml 0.18.0",
- "clap 4.5.4",
+ "clap 4.5.7",
  "once_cell",
  "serde",
  "serde_json",
@@ -2417,6 +2417,7 @@ dependencies = [
  "base64 0.21.7",
  "bytes",
  "ciborium",
+ "clap 4.5.7",
  "dioxus",
  "dioxus-cli-config",
  "dioxus-desktop",

+ 9 - 0
packages/cli-config/src/lib.rs

@@ -8,6 +8,10 @@ mod bundle;
 pub use bundle::*;
 mod cargo;
 pub use cargo::*;
+#[cfg(feature = "cli")]
+mod serve;
+#[cfg(feature = "cli")]
+pub use serve::*;
 
 #[doc(hidden)]
 pub mod __private {
@@ -28,6 +32,11 @@ pub mod __private {
             std::env::remove_var(CONFIG_ENV);
         }
     }
+
+    #[cfg(feature = "cli")]
+    /// The environment variable that stores the CLIs serve configuration.
+    /// We use this to communicate between the CLI and the server for fullstack applications.
+    pub const SERVE_ENV: &str = "DIOXUS_SERVE_CONFIG";
 }
 
 /// An error that occurs when the dioxus CLI was not used to build the application.

+ 35 - 0
packages/cli-config/src/serve.rs

@@ -0,0 +1,35 @@
+use clap::Parser;
+
+/// Arguments for the serve command
+#[derive(Clone, Debug, Parser, serde::Serialize, serde::Deserialize)]
+pub struct ServeArguments {
+    /// The port the server will run on
+    #[clap(long)]
+    #[clap(default_value_t = default_port())]
+    pub port: u16,
+    /// The address the server will run on
+    #[clap(long)]
+    pub addr: Option<std::net::IpAddr>,
+}
+
+impl Default for ServeArguments {
+    fn default() -> Self {
+        Self {
+            port: default_port(),
+            addr: None,
+        }
+    }
+}
+
+impl ServeArguments {
+    /// Attempt to read the current serve settings from the CLI. This will only be set for the fullstack platform on recent versions of the CLI.
+    pub fn from_cli() -> Option<Self> {
+        std::env::var(crate::__private::SERVE_ENV)
+            .ok()
+            .and_then(|json| serde_json::from_str(&json).ok())
+    }
+}
+
+fn default_port() -> u16 {
+    8080
+}

+ 8 - 4
packages/cli/src/cli/cfg.rs

@@ -1,4 +1,5 @@
 use dioxus_cli_config::Platform;
+use dioxus_cli_config::ServeArguments;
 
 use super::*;
 
@@ -80,10 +81,9 @@ impl From<ConfigOptsServe> for ConfigOptsBuild {
 #[derive(Clone, Debug, Default, Deserialize, Parser)]
 #[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))]
 pub struct ConfigOptsServe {
-    /// Port of dev server
-    #[clap(long)]
-    #[clap(default_value_t = 8080)]
-    pub port: u16,
+    /// Arguments for the serve command
+    #[clap(flatten)]
+    pub(crate) server_arguments: ServeArguments,
 
     /// Open the app in the default browser [default: true]
     #[clap(long, default_value_t = false)]
@@ -156,6 +156,10 @@ pub struct ConfigOptsServe {
     #[clap(long)]
     pub target: Option<String>,
 
+    /// Additional arguments to pass to the executable
+    #[clap(long)]
+    pub args: Vec<String>,
+
     /// Extra arguments passed to cargo build
     #[clap(last = true)]
     pub cargo_args: Vec<String>,

+ 32 - 8
packages/cli/src/server/desktop/mod.rs

@@ -91,7 +91,7 @@ async fn serve<P: Platform + Send + 'static>(
         }
     });
 
-    let platform = RwLock::new(P::start(&config, serve)?);
+    let platform = RwLock::new(P::start(&config, serve, Vec::new())?);
 
     tracing::info!("🚀 Starting development server...");
 
@@ -100,7 +100,13 @@ async fn serve<P: Platform + Send + 'static>(
     let _watcher = setup_file_watcher(
         {
             let config = config.clone();
-            move || platform.write().unwrap().rebuild(&config)
+            let serve = serve.clone();
+            move || {
+                platform
+                    .write()
+                    .unwrap()
+                    .rebuild(&config, &serve, Vec::new())
+            }
         },
         &config,
         None,
@@ -238,6 +244,8 @@ fn start_desktop(
     config: &CrateConfig,
     skip_assets: bool,
     rust_flags: Option<String>,
+    args: &Vec<String>,
+    env: Vec<(String, String)>,
 ) -> Result<(RAIIChild, BuildResult)> {
     // Run the desktop application
     // Only used for the fullstack platform,
@@ -251,7 +259,9 @@ fn start_desktop(
                 .clone()
                 .ok_or(anyhow::anyhow!("No executable found after desktop build"))?,
         )
+        .args(args)
         .env(active, "true")
+        .envs(env)
         .spawn()?,
     );
 
@@ -259,6 +269,7 @@ fn start_desktop(
 }
 
 pub(crate) struct DesktopPlatform {
+    args: Vec<String>,
     currently_running_child: RAIIChild,
     skip_assets: bool,
 }
@@ -270,8 +281,10 @@ impl DesktopPlatform {
         config: &CrateConfig,
         serve: &ConfigOptsServe,
         rust_flags: Option<String>,
+        env: Vec<(String, String)>,
     ) -> Result<Self> {
-        let (child, first_build_result) = start_desktop(config, serve.skip_assets, rust_flags)?;
+        let (child, first_build_result) =
+            start_desktop(config, serve.skip_assets, rust_flags, &serve.args, env)?;
 
         tracing::info!("🚀 Starting development server...");
 
@@ -287,6 +300,7 @@ impl DesktopPlatform {
         );
 
         Ok(Self {
+            args: serve.args.clone(),
             currently_running_child: child,
             skip_assets: serve.skip_assets,
         })
@@ -298,6 +312,7 @@ impl DesktopPlatform {
         &mut self,
         config: &CrateConfig,
         rust_flags: Option<String>,
+        env: Vec<(String, String)>,
     ) -> Result<BuildResult> {
         // Gracefully shtudown the desktop app
         // It might have a receiver to do some cleanup stuff
@@ -322,27 +337,36 @@ impl DesktopPlatform {
         // Todo: add a timeout here to kill the process if it doesn't shut down within a reasonable time
         self.currently_running_child.0.wait()?;
 
-        let (child, result) = start_desktop(config, self.skip_assets, rust_flags)?;
+        let (child, result) = start_desktop(config, self.skip_assets, rust_flags, &self.args, env)?;
         self.currently_running_child = child;
         Ok(result)
     }
 }
 
 impl Platform for DesktopPlatform {
-    fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self> {
+    fn start(
+        config: &CrateConfig,
+        serve: &ConfigOptsServe,
+        env: Vec<(String, String)>,
+    ) -> Result<Self> {
         // See `start_with_options()`'s docs for the explanation why the code
         // was moved there.
         // Since desktop platform doesn't use `rust_flags`, this argument is
         // explicitly set to `None`.
-        DesktopPlatform::start_with_options(config, serve, None)
+        DesktopPlatform::start_with_options(config, serve, None, env)
     }
 
-    fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult> {
+    fn rebuild(
+        &mut self,
+        config: &CrateConfig,
+        _: &ConfigOptsServe,
+        env: Vec<(String, String)>,
+    ) -> Result<BuildResult> {
         // See `rebuild_with_options()`'s docs for the explanation why the code
         // was moved there.
         // Since desktop platform doesn't use `rust_flags`, this argument is
         // explicitly set to `None`.
-        DesktopPlatform::rebuild_with_options(self, config, None)
+        DesktopPlatform::rebuild_with_options(self, config, None, env)
     }
 }
 

+ 41 - 6
packages/cli/src/server/fullstack/mod.rs

@@ -2,7 +2,7 @@ use dioxus_cli_config::CrateConfig;
 
 use crate::{
     cfg::{ConfigOptsBuild, ConfigOptsServe},
-    Result,
+    BuildResult, Result,
 };
 
 use super::{desktop, Platform};
@@ -61,6 +61,13 @@ fn make_desktop_config(config: &CrateConfig, serve: &ConfigOptsServe) -> CrateCo
     desktop_config
 }
 
+fn add_serve_options_to_env(serve: &ConfigOptsServe, env: &mut Vec<(String, String)>) {
+    env.push((
+        dioxus_cli_config::__private::SERVE_ENV.to_string(),
+        serde_json::to_string(&serve.server_arguments).unwrap(),
+    ));
+}
+
 struct FullstackPlatform {
     serve: ConfigOptsServe,
     desktop: desktop::DesktopPlatform,
@@ -68,7 +75,11 @@ struct FullstackPlatform {
 }
 
 impl Platform for FullstackPlatform {
-    fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
+    fn start(
+        config: &CrateConfig,
+        serve: &ConfigOptsServe,
+        env: Vec<(String, String)>,
+    ) -> Result<Self>
     where
         Self: Sized,
     {
@@ -76,15 +87,30 @@ impl Platform for FullstackPlatform {
 
         let desktop_config = make_desktop_config(config, serve);
         let server_rust_flags = server_rust_flags(&serve.clone().into());
+        let mut desktop_env = env.clone();
+        add_serve_options_to_env(serve, &mut desktop_env);
         let desktop = desktop::DesktopPlatform::start_with_options(
             &desktop_config,
             serve,
             Some(server_rust_flags.clone()),
+            desktop_env,
         )?;
         thread_handle
             .join()
             .map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
 
+        if serve.open {
+            crate::server::web::open_browser(
+                config,
+                serve
+                    .server_arguments
+                    .addr
+                    .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))),
+                serve.server_arguments.port,
+                false,
+            );
+        }
+
         Ok(Self {
             desktop,
             serve: serve.clone(),
@@ -92,12 +118,21 @@ impl Platform for FullstackPlatform {
         })
     }
 
-    fn rebuild(&mut self, crate_config: &CrateConfig) -> Result<crate::BuildResult> {
+    fn rebuild(
+        &mut self,
+        crate_config: &CrateConfig,
+        serve: &ConfigOptsServe,
+        env: Vec<(String, String)>,
+    ) -> Result<BuildResult> {
         let thread_handle = start_web_build_thread(crate_config, &self.serve);
         let desktop_config = make_desktop_config(crate_config, &self.serve);
-        let result = self
-            .desktop
-            .rebuild_with_options(&desktop_config, Some(self.server_rust_flags.clone()));
+        let mut desktop_env = env.clone();
+        add_serve_options_to_env(serve, &mut desktop_env);
+        let result = self.desktop.rebuild_with_options(
+            &desktop_config,
+            Some(self.server_rust_flags.clone()),
+            desktop_env,
+        );
         thread_handle
             .join()
             .map_err(|_| anyhow::anyhow!("Failed to join thread"))??;

+ 11 - 2
packages/cli/src/server/mod.rs

@@ -309,10 +309,19 @@ fn local_path_of_asset(path: &Path) -> Option<PathBuf> {
 }
 
 pub(crate) trait Platform {
-    fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
+    fn start(
+        config: &CrateConfig,
+        serve: &ConfigOptsServe,
+        env: Vec<(String, String)>,
+    ) -> Result<Self>
     where
         Self: Sized;
-    fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
+    fn rebuild(
+        &mut self,
+        config: &CrateConfig,
+        serve: &ConfigOptsServe,
+        env: Vec<(String, String)>,
+    ) -> Result<BuildResult>;
 }
 
 fn is_backup_file(path: &Path) -> bool {

+ 20 - 23
packages/cli/src/server/output.rs

@@ -1,7 +1,7 @@
 use crate::server::Diagnostic;
 use colored::Colorize;
 use dioxus_cli_config::{crate_root, CrateConfig};
-use std::{path::PathBuf, process::Command};
+use std::{net::IpAddr, path::PathBuf, process::Command};
 
 #[derive(Debug, Default)]
 pub struct PrettierOptions {
@@ -12,7 +12,7 @@ pub struct PrettierOptions {
 
 #[derive(Debug, Clone)]
 pub struct WebServerInfo {
-    pub ip: String,
+    pub ip: IpAddr,
     pub port: u16,
 }
 
@@ -77,27 +77,24 @@ pub fn print_console_info(
     }
 
     if let Some(WebServerInfo { ip, port }) = web_info {
-        if config.dioxus_config.web.https.enabled == Some(true) {
-            println!(
-                "    > Local address: {}",
-                format!("https://localhost:{}/", port).blue()
-            );
-            println!(
-                "    > Network address: {}",
-                format!("https://{}:{}/", ip, port).blue()
-            );
-            println!("    > HTTPS: {}", "Enabled".to_string().green());
-        } else {
-            println!(
-                "    > Local address: {}",
-                format!("http://localhost:{}/", port).blue()
-            );
-            println!(
-                "    > Network address: {}",
-                format!("http://{}:{}/", ip, port).blue()
-            );
-            println!("    > HTTPS status: {}", "Disabled".to_string().red());
-        }
+        let https = config.dioxus_config.web.https.enabled == Some(true);
+        let prefix = if https { "https://" } else { "http://" };
+        println!(
+            "    > Local address: {}",
+            format!("{prefix}localhost:{}/", port).blue()
+        );
+        println!(
+            "    > Network address: {}",
+            format!("{prefix}{}:{}/", ip, port).blue()
+        );
+        println!(
+            "    > HTTPS: {}",
+            if https {
+                "Enabled".to_string().green()
+            } else {
+                "Disabled".to_string().red()
+            }
+        );
     }
     println!();
 

+ 26 - 26
packages/cli/src/server/web/mod.rs

@@ -11,7 +11,7 @@ use crate::{
 use dioxus_cli_config::CrateConfig;
 use dioxus_rsx::hot_reload::*;
 use std::{
-    net::{SocketAddr, UdpSocket},
+    net::{IpAddr, SocketAddr, UdpSocket},
     sync::Arc,
 };
 
@@ -25,7 +25,11 @@ use super::HotReloadState;
 pub async fn startup(config: CrateConfig, serve_cfg: &ConfigOptsServe) -> Result<()> {
     set_ctrlc_handler(&config);
 
-    let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
+    let ip = serve_cfg
+        .server_arguments
+        .addr
+        .or_else(get_ip)
+        .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)));
 
     let hot_reload_state = build_hotreload_filemap(&config);
 
@@ -34,13 +38,13 @@ pub async fn startup(config: CrateConfig, serve_cfg: &ConfigOptsServe) -> Result
 
 /// Start the server without hot reload
 pub async fn serve(
-    ip: String,
+    ip: IpAddr,
     config: CrateConfig,
     hot_reload_state: HotReloadState,
     opts: &ConfigOptsServe,
 ) -> Result<()> {
     let skip_assets = opts.skip_assets;
-    let port = opts.port;
+    let port = opts.server_arguments.port;
 
     // Since web platform doesn't use `rust_flags`, this argument is explicitly
     // set to `None`.
@@ -60,10 +64,7 @@ pub async fn serve(
             move || build(&config, &hot_reload_state, skip_assets)
         },
         &config,
-        Some(WebServerInfo {
-            ip: ip.clone(),
-            port,
-        }),
+        Some(WebServerInfo { ip, port }),
         hot_reload_state.clone(),
     )
     .await?;
@@ -80,23 +81,21 @@ pub async fn serve(
             warnings: first_build_result.warnings,
             elapsed_time: first_build_result.elapsed_time,
         },
-        Some(WebServerInfo {
-            ip: ip.clone(),
-            port,
-        }),
+        Some(WebServerInfo { ip, port }),
     );
 
     // Router
     let router = setup_router(config.clone(), hot_reload_state).await?;
 
     // Start server
-    start_server(port, router, opts.open, rustls_config, &config).await?;
+    start_server(ip, port, router, opts.open, rustls_config, &config).await?;
 
     Ok(())
 }
 
 /// Starts dx serve with no hot reload
 async fn start_server(
+    ip: IpAddr,
     port: u16,
     router: axum::Router,
     start_browser: bool,
@@ -107,20 +106,11 @@ async fn start_server(
     #[cfg(feature = "plugin")]
     crate::plugin::PluginManager::on_serve_start(config)?;
 
-    // Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack)
-    let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
+    let addr: SocketAddr = SocketAddr::from((ip, port));
 
     // Open the browser
     if start_browser {
-        let protocol = match rustls {
-            Some(_) => "https",
-            None => "http",
-        };
-        let base_path = match config.dioxus_config.web.app.base_path.as_deref() {
-            Some(base_path) => format!("/{}", base_path.trim_matches('/')),
-            None => "".to_owned(),
-        };
-        _ = open::that(format!("{protocol}://localhost:{port}{base_path}"));
+        open_browser(config, ip, port, rustls.is_some());
     }
 
     let svc = router.into_make_service();
@@ -138,8 +128,18 @@ async fn start_server(
     Ok(())
 }
 
+/// Open the browser to the address
+pub(crate) fn open_browser(config: &CrateConfig, ip: IpAddr, port: u16, https: bool) {
+    let protocol = if https { "https" } else { "http" };
+    let base_path = match config.dioxus_config.web.app.base_path.as_deref() {
+        Some(base_path) => format!("/{}", base_path.trim_matches('/')),
+        None => "".to_owned(),
+    };
+    _ = open::that(format!("{protocol}://{ip}:{port}{base_path}"));
+}
+
 /// Get the network ip
-fn get_ip() -> Option<String> {
+fn get_ip() -> Option<IpAddr> {
     let socket = match UdpSocket::bind("0.0.0.0:0") {
         Ok(s) => s,
         Err(_) => return None,
@@ -151,7 +151,7 @@ fn get_ip() -> Option<String> {
     };
 
     match socket.local_addr() {
-        Ok(addr) => Some(addr.ip().to_string()),
+        Ok(addr) => Some(addr.ip()),
         Err(_) => None,
     }
 }

+ 55 - 19
packages/dioxus/src/launch.rs

@@ -156,13 +156,13 @@ impl<Cfg> LaunchBuilder<Cfg, SendContext> {
 /// - Any config will be converted into `Some(config)`
 /// - If the config is for another platform, it will be converted into `None`
 pub trait TryIntoConfig<Config = Self> {
-    fn into_config(self) -> Option<Config>;
+    fn into_config(self, config: &mut Option<Config>);
 }
 
 // A config can always be converted into itself
 impl<Cfg> TryIntoConfig<Cfg> for Cfg {
-    fn into_config(self) -> Option<Cfg> {
-        Some(self)
+    fn into_config(self, config: &mut Option<Cfg>) {
+        *config = Some(self);
     }
 }
 
@@ -176,15 +176,13 @@ impl<Cfg> TryIntoConfig<Cfg> for Cfg {
     feature = "fullstack"
 ))]
 impl TryIntoConfig<current_platform::Config> for () {
-    fn into_config(self) -> Option<current_platform::Config> {
-        None
-    }
+    fn into_config(self, config: &mut Option<current_platform::Config>) {}
 }
 
 impl<Cfg: Default + 'static, ContextFn: ?Sized> LaunchBuilder<Cfg, ContextFn> {
     /// Provide a platform-specific config to the builder.
     pub fn with_cfg(mut self, config: impl TryIntoConfig<Cfg>) -> Self {
-        self.platform_config = self.platform_config.or(config.into_config());
+        config.into_config(&mut self.platform_config);
         self
     }
 
@@ -234,6 +232,21 @@ mod current_platform {
     #[cfg(feature = "fullstack")]
     pub use dioxus_fullstack::launch::*;
 
+    #[cfg(all(feature = "fullstack", feature = "axum"))]
+    impl TryIntoConfig<crate::launch::current_platform::Config>
+        for ::dioxus_fullstack::prelude::ServeConfigBuilder
+    {
+        fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {
+            match config {
+                Some(config) => config.set_server_cfg(self),
+                None => {
+                    *config =
+                        Some(crate::launch::current_platform::Config::new().with_server_cfg(self))
+                }
+            }
+        }
+    }
+
     #[cfg(any(feature = "desktop", feature = "mobile"))]
     if_else_cfg! {
         if not(feature = "fullstack") {
@@ -242,9 +255,25 @@ mod current_platform {
             #[cfg(not(feature = "desktop"))]
             pub use dioxus_mobile::launch::*;
         } else {
-            impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_desktop::Config {
-                fn into_config(self) -> Option<crate::launch::current_platform::Config> {
-                    None
+            if_else_cfg! {
+                if feature = "desktop" {
+                    impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_desktop::Config {
+                        fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {
+                            match config {
+                                Some(config) => config.set_desktop_config(self),
+                                None => *config = Some(crate::launch::current_platform::Config::new().with_desktop_config(self)),
+                            }
+                        }
+                    }
+                } else {
+                    impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_mobile::Config {
+                        fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {
+                            match config {
+                                Some(config) => config.set_mobile_cfg(self),
+                                None => *config = Some(crate::launch::current_platform::Config::new().with_mobile_cfg(self)),
+                            }
+                        }
+                    }
                 }
             }
         }
@@ -256,9 +285,7 @@ mod current_platform {
             pub use dioxus_static_site_generation::launch::*;
         } else {
             impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_static_site_generation::Config {
-                fn into_config(self) -> Option<crate::launch::current_platform::Config> {
-                    None
-                }
+                fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {}
             }
         }
     }
@@ -268,9 +295,20 @@ mod current_platform {
         if not(any(feature = "desktop", feature = "mobile", feature = "fullstack", feature = "static-generation")) {
             pub use dioxus_web::launch::*;
         } else {
-            impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_web::Config {
-                fn into_config(self) -> Option<crate::launch::current_platform::Config> {
-                    None
+            if_else_cfg! {
+                if feature = "fullstack" {
+                    impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_web::Config {
+                        fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {
+                            match config {
+                                Some(config) => config.set_web_config(self),
+                                None => *config = Some(crate::launch::current_platform::Config::new().with_web_config(self)),
+                            }
+                        }
+                    }
+                } else {
+                    impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_web::Config {
+                        fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {}
+                    }
                 }
             }
         }
@@ -290,9 +328,7 @@ mod current_platform {
             pub use dioxus_liveview::launch::*;
         } else {
             impl<R: ::dioxus_liveview::LiveviewRouter> TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_liveview::Config<R> {
-                fn into_config(self) -> Option<crate::launch::current_platform::Config> {
-                    None
-                }
+                fn into_config(self, config: &mut Option<crate::launch::current_platform::Config>) {}
             }
         }
     }

+ 4 - 1
packages/fullstack/Cargo.toml

@@ -56,6 +56,7 @@ tower-layer = { version = "0.3.2", optional = true }
 web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
 
 dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true }
+clap = { version = "4.5.7", optional = true, features = ["derive"] }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 tokio = { workspace = true, features = ["rt", "sync"], optional = true }
@@ -92,7 +93,9 @@ server = [
     "dep:tracing-futures",
     "dep:pin-project",
     "dep:thiserror",
-    "dep:dioxus-cli-config"
+    "dep:dioxus-cli-config",
+    "dep:clap",
+    "dioxus-cli-config/cli"
 ]
 
 [package.metadata.docs.rs]

+ 6 - 5
packages/fullstack/examples/axum-router/src/main.rs

@@ -7,11 +7,12 @@
 use dioxus::prelude::*;
 
 fn main() {
-    let cfg = server_only!(dioxus::fullstack::Config::new().incremental(
-        IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
-    ));
-
-    LaunchBuilder::fullstack().with_cfg(cfg).launch(app);
+    LaunchBuilder::fullstack()
+        .with_cfg(server_only!(ServeConfig::builder().incremental(
+            IncrementalRendererConfig::default()
+                .invalidate_after(std::time::Duration::from_secs(120)),
+        )))
+        .launch(app);
 }
 
 fn app() -> Element {

+ 26 - 79
packages/fullstack/src/config.rs

@@ -6,15 +6,9 @@ use std::sync::Arc;
 
 /// Settings for a fullstack app.
 pub struct Config {
-    #[cfg(feature = "server")]
-    pub(crate) server_fn_route: &'static str,
-
     #[cfg(feature = "server")]
     pub(crate) server_cfg: ServeConfigBuilder,
 
-    #[cfg(feature = "server")]
-    pub(crate) addr: std::net::SocketAddr,
-
     #[cfg(feature = "web")]
     pub(crate) web_cfg: dioxus_web::Config,
 
@@ -29,10 +23,6 @@ pub struct Config {
 impl Default for Config {
     fn default() -> Self {
         Self {
-            #[cfg(feature = "server")]
-            server_fn_route: "",
-            #[cfg(feature = "server")]
-            addr: std::net::SocketAddr::from(([0, 0, 0, 0], 8080)),
             #[cfg(feature = "server")]
             server_cfg: ServeConfigBuilder::new(),
             #[cfg(feature = "web")]
@@ -51,24 +41,6 @@ impl Config {
         Self::default()
     }
 
-    /// Set the address to serve the app on.
-    #[cfg(feature = "server")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
-    pub fn addr(self, addr: impl Into<std::net::SocketAddr>) -> Self {
-        let addr = addr.into();
-        Self { addr, ..self }
-    }
-
-    /// Set the route to the server functions.
-    #[cfg(feature = "server")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
-    pub fn server_fn_route(self, server_fn_route: &'static str) -> Self {
-        Self {
-            server_fn_route,
-            ..self
-        }
-    }
-
     /// Set the incremental renderer config.
     #[cfg(feature = "server")]
     #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
@@ -82,77 +54,52 @@ impl Config {
     /// Set the server config.
     #[cfg(feature = "server")]
     #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
-    pub fn server_cfg(self, server_cfg: ServeConfigBuilder) -> Self {
+    pub fn with_server_cfg(self, server_cfg: ServeConfigBuilder) -> Self {
         Self { server_cfg, ..self }
     }
 
+    /// Set the server config.
+    #[cfg(feature = "server")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
+    pub fn set_server_cfg(&mut self, server_cfg: ServeConfigBuilder) {
+        self.server_cfg = server_cfg;
+    }
+
     /// Set the web config.
     #[cfg(feature = "web")]
     #[cfg_attr(docsrs, doc(cfg(feature = "web")))]
-    pub fn web_cfg(self, web_cfg: dioxus_web::Config) -> Self {
+    pub fn with_web_config(self, web_cfg: dioxus_web::Config) -> Self {
         Self { web_cfg, ..self }
     }
 
-    /// Set the desktop config.
+    /// Set the web config.
+    #[cfg(feature = "web")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "web")))]
+    pub fn set_web_config(&mut self, web_cfg: dioxus_web::Config) {
+        self.web_cfg = web_cfg;
+    }
+
+    /// Set the desktop config
     #[cfg(feature = "desktop")]
     #[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
-    pub fn desktop_cfg(self, desktop_cfg: dioxus_desktop::Config) -> Self {
+    pub fn with_desktop_config(self, desktop_cfg: dioxus_desktop::Config) -> Self {
         Self {
             desktop_cfg,
             ..self
         }
     }
 
+    /// Set the desktop config.
+    #[cfg(feature = "desktop")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
+    pub fn set_desktop_config(&mut self, desktop_cfg: dioxus_desktop::Config) {
+        self.desktop_cfg = desktop_cfg;
+    }
+
     /// Set the mobile config.
     #[cfg(feature = "mobile")]
     #[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
-    pub fn mobile_cfg(self, mobile_cfg: dioxus_mobile::Config) -> Self {
+    pub fn with_mobile_cfg(self, mobile_cfg: dioxus_mobile::Config) -> Self {
         Self { mobile_cfg, ..self }
     }
-
-    #[cfg(feature = "server")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "server")))]
-    /// Launch a server application
-    pub async fn launch_server(
-        self,
-        build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
-        context_providers: crate::launch::ContextProviders,
-    ) {
-        use std::any::Any;
-
-        let addr = self.addr;
-        println!("Listening on http://{}", addr);
-        let cfg = self.server_cfg.build();
-        let server_fn_route = self.server_fn_route;
-
-        #[cfg(feature = "axum")]
-        {
-            use crate::axum_adapter::{render_handler, DioxusRouterExt};
-            use axum::routing::get;
-            use tower::ServiceBuilder;
-
-            let ssr_state = SSRState::new(&cfg);
-            let router =
-                axum::Router::new().register_server_functions_with_context(context_providers);
-            #[cfg(not(any(feature = "desktop", feature = "mobile")))]
-            let router = {
-                let mut router = router.serve_static_assets(cfg.assets_path.clone()).await;
-
-                #[cfg(all(feature = "hot-reload", debug_assertions))]
-                {
-                    use dioxus_hot_reload::HotReloadRouterExt;
-                    router = router.forward_cli_hot_reloading();
-                }
-
-                router.fallback(get(render_handler).with_state((
-                    cfg,
-                    Arc::new(build_virtual_dom),
-                    ssr_state,
-                )))
-            };
-            let router = router.into_make_service();
-            let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
-            axum::serve(listener, router).await.unwrap();
-        }
-    }
 }

+ 65 - 6
packages/fullstack/src/launch.rs

@@ -36,7 +36,7 @@ pub fn launch(
     tokio::runtime::Runtime::new()
         .unwrap()
         .block_on(async move {
-            platform_config.launch_server(factory, contexts).await;
+            launch_server(platform_config, factory, contexts).await;
         });
 
     unreachable!("Launching a fullstack app should never return")
@@ -50,7 +50,8 @@ pub fn launch(
     contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
     platform_config: Config,
 ) {
-    let factory = virtual_dom_factory(root, Arc::new(contexts));
+    let contexts = Arc::new(contexts);
+    let factory = virtual_dom_factory(root, contexts);
     let cfg = platform_config.web_cfg.hydrate(true);
     dioxus_web::launch::launch_virtual_dom(factory(), cfg)
 }
@@ -63,7 +64,8 @@ pub fn launch(
     contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
     platform_config: Config,
 ) -> ! {
-    let factory = virtual_dom_factory(root, Arc::new(contexts));
+    let contexts = Arc::new(contexts);
+    let factory = virtual_dom_factory(root, contexts);
     let cfg = platform_config.desktop_cfg;
     dioxus_desktop::launch::launch_virtual_dom(factory(), cfg)
 }
@@ -76,10 +78,67 @@ pub fn launch(
 #[allow(unused)]
 pub fn launch(
     root: fn() -> Element,
-    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+    contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
     platform_config: Config,
-) {
-    let factory = virtual_dom_factory(root, Arc::new(contexts));
+) -> ! {
+    let contexts = Arc::new(contexts);
+    let factory = virtual_dom_factory(root, contexts.clone());
     let cfg = platform_config.mobile_cfg;
     dioxus_mobile::launch::launch_virtual_dom(factory(), cfg)
 }
+
+#[cfg(feature = "server")]
+#[allow(unused)]
+/// Launch a server application
+async fn launch_server(
+    platform_config: Config,
+    build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
+    context_providers: ContextProviders,
+) {
+    use clap::Parser;
+
+    let args = dioxus_cli_config::ServeArguments::from_cli()
+        .unwrap_or_else(dioxus_cli_config::ServeArguments::parse);
+    let addr = args
+        .addr
+        .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)));
+    let addr = std::net::SocketAddr::new(addr, args.port);
+    println!("Listening on http://{}", addr);
+
+    #[cfg(feature = "axum")]
+    {
+        use crate::axum_adapter::DioxusRouterExt;
+
+        let router = axum::Router::new().register_server_functions_with_context(context_providers);
+        #[cfg(not(any(feature = "desktop", feature = "mobile")))]
+        let router = {
+            use crate::prelude::SSRState;
+
+            let cfg = platform_config.server_cfg.build();
+
+            let ssr_state = SSRState::new(&cfg);
+            let mut router = router.serve_static_assets(cfg.assets_path.clone()).await;
+
+            #[cfg(all(feature = "hot-reload", debug_assertions))]
+            {
+                use dioxus_hot_reload::HotReloadRouterExt;
+                router = router.forward_cli_hot_reloading();
+            }
+
+            router.fallback(
+                axum::routing::get(crate::axum_adapter::render_handler).with_state((
+                    cfg,
+                    Arc::new(build_virtual_dom),
+                    ssr_state,
+                )),
+            )
+        };
+        let router = router.into_make_service();
+        let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
+        axum::serve(listener, router).await.unwrap();
+    }
+    #[cfg(not(feature = "axum"))]
+    {
+        panic!("Launching with dioxus fullstack requires the axum feature. If you are using a community fullstack adapter, please check the documentation for that adapter to see how to launch the application.");
+    }
+}

+ 2 - 0
packages/fullstack/src/lib.rs

@@ -23,6 +23,8 @@ mod render;
 
 #[cfg(feature = "server")]
 mod serve_config;
+#[cfg(feature = "server")]
+pub use serve_config::*;
 
 #[cfg(feature = "server")]
 mod server_context;

+ 1 - 5
packages/playwright-tests/fullstack/src/main.rs

@@ -8,11 +8,7 @@
 use dioxus::prelude::*;
 
 fn main() {
-    LaunchBuilder::fullstack()
-        .with_cfg(ssr! {
-            dioxus::fullstack::Config::default().addr(std::net::SocketAddr::from(([127, 0, 0, 1], 3333)))
-        })
-        .launch(app);
+    LaunchBuilder::fullstack().launch(app);
 }
 
 fn app() -> Element {

+ 6 - 4
packages/playwright-tests/playwright.config.js

@@ -82,15 +82,17 @@ module.exports = defineConfig({
     },
     {
       cwd: path.join(process.cwd(), "web"),
-      command: "cargo run --package dioxus-cli --release -- serve",
-      port: 8080,
+      command:
+        'cargo run --package dioxus-cli --release -- serve --addr "127.0.0.1" --port 9999',
+      port: 9999,
       timeout: 20 * 60 * 1000,
       reuseExistingServer: !process.env.CI,
       stdout: "pipe",
     },
     {
-      cwd: path.join(process.cwd(), 'fullstack'),
-      command: 'cargo run --package dioxus-cli --release -- serve --platform fullstack',
+      cwd: path.join(process.cwd(), "fullstack"),
+      command:
+        'cargo run --package dioxus-cli --release -- serve --platform fullstack --addr "127.0.0.1" --port 3333',
       port: 3333,
       timeout: 20 * 60 * 1000,
       reuseExistingServer: !process.env.CI,

+ 45 - 46
packages/playwright-tests/web.spec.js

@@ -1,94 +1,93 @@
 // @ts-check
-const { test, expect, defineConfig } = require('@playwright/test');
+const { test, expect, defineConfig } = require("@playwright/test");
 
-test('button click', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("button click", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the counter text.
-  const main = page.locator('#main');
-  await expect(main).toContainText('hello axum! 0');
+  const main = page.locator("#main");
+  await expect(main).toContainText("hello axum! 0");
 
   // Click the increment button.
-  let button = page.locator('button.increment-button');
+  let button = page.locator("button.increment-button");
   await button.click();
 
   // Expect the page to contain the updated counter text.
-  await expect(main).toContainText('hello axum! 1');
+  await expect(main).toContainText("hello axum! 1");
 });
 
-test('svg', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("svg", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the svg.
-  const svg = page.locator('svg');
+  const svg = page.locator("svg");
 
   // Expect the svg to contain the circle.
-  const circle = svg.locator('circle');
-  await expect(circle).toHaveAttribute('cx', '50');
-  await expect(circle).toHaveAttribute('cy', '50');
-  await expect(circle).toHaveAttribute('r', '40');
-  await expect(circle).toHaveAttribute('stroke', 'green');
-  await expect(circle).toHaveAttribute('fill', 'yellow');
+  const circle = svg.locator("circle");
+  await expect(circle).toHaveAttribute("cx", "50");
+  await expect(circle).toHaveAttribute("cy", "50");
+  await expect(circle).toHaveAttribute("r", "40");
+  await expect(circle).toHaveAttribute("stroke", "green");
+  await expect(circle).toHaveAttribute("fill", "yellow");
 });
 
-test('raw attribute', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("raw attribute", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the div with the raw attribute.
-  const div = page.locator('div.raw-attribute-div');
-  await expect(div).toHaveAttribute('raw-attribute', 'raw-attribute-value');
+  const div = page.locator("div.raw-attribute-div");
+  await expect(div).toHaveAttribute("raw-attribute", "raw-attribute-value");
 });
 
-test('hidden attribute', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("hidden attribute", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the div with the hidden attribute.
-  const div = page.locator('div.hidden-attribute-div');
-  await expect(div).toHaveAttribute('hidden', 'true');
+  const div = page.locator("div.hidden-attribute-div");
+  await expect(div).toHaveAttribute("hidden", "true");
 });
 
-test('dangerous inner html', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("dangerous inner html", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the div with the dangerous inner html.
-  const div = page.locator('div.dangerous-inner-html-div');
-  await expect(div).toContainText('hello dangerous inner html');
+  const div = page.locator("div.dangerous-inner-html-div");
+  await expect(div).toContainText("hello dangerous inner html");
 });
 
-test('input value', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("input value", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the input with the value.
-  const input = page.locator('input');
-  await expect(input).toHaveValue('hello input');
+  const input = page.locator("input");
+  await expect(input).toHaveValue("hello input");
 });
 
-test('style', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("style", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the div with the style.
-  const div = page.locator('div.style-div');
-  await expect(div).toHaveText('colored text');
-  await expect(div).toHaveCSS('color', 'rgb(255, 0, 0)');
+  const div = page.locator("div.style-div");
+  await expect(div).toHaveText("colored text");
+  await expect(div).toHaveCSS("color", "rgb(255, 0, 0)");
 });
 
-test('eval', async ({ page }) => {
-  await page.goto('http://localhost:8080');
+test("eval", async ({ page }) => {
+  await page.goto("http://localhost:9999");
 
   // Expect the page to contain the div with the eval and have no text.
-  const div = page.locator('div.eval-result');
-  await expect(div).toHaveText('');
+  const div = page.locator("div.eval-result");
+  await expect(div).toHaveText("");
 
   // Click the button to run the eval.
-  let button = page.locator('button.eval-button');
+  let button = page.locator("button.eval-button");
   await button.click();
 
   // Check that the title changed.
-  await expect(page).toHaveTitle('Hello from Dioxus Eval!');
+  await expect(page).toHaveTitle("Hello from Dioxus Eval!");
 
   // Check that the div has the eval value.
-  await expect(div).toHaveText('returned eval value');
-
+  await expect(div).toHaveText("returned eval value");
 });
 
-// Shutdown the li
+// Shutdown the li