diff --git a/Cargo.lock b/Cargo.lock index 1b2b863dc..cbec509f0 100644 --- a/Cargo.lock +++ b/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", diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index 60952555d..db10f70e2 100644 --- a/packages/cli-config/src/lib.rs +++ b/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. diff --git a/packages/cli-config/src/serve.rs b/packages/cli-config/src/serve.rs new file mode 100644 index 000000000..c3577c867 --- /dev/null +++ b/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, +} + +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 { + std::env::var(crate::__private::SERVE_ENV) + .ok() + .and_then(|json| serde_json::from_str(&json).ok()) + } +} + +fn default_port() -> u16 { + 8080 +} diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index e4490fb33..fc05f5aee 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/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 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, + /// Additional arguments to pass to the executable + #[clap(long)] + pub args: Vec, + /// Extra arguments passed to cargo build #[clap(last = true)] pub cargo_args: Vec, diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 678772c6c..8527e2bb9 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -91,7 +91,7 @@ async fn serve( } }); - 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( 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, + args: &Vec, + 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, currently_running_child: RAIIChild, skip_assets: bool, } @@ -270,8 +281,10 @@ impl DesktopPlatform { config: &CrateConfig, serve: &ConfigOptsServe, rust_flags: Option, + env: Vec<(String, String)>, ) -> Result { - 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, + env: Vec<(String, String)>, ) -> Result { // 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 { + fn start( + config: &CrateConfig, + serve: &ConfigOptsServe, + env: Vec<(String, String)>, + ) -> Result { // 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 { + fn rebuild( + &mut self, + config: &CrateConfig, + _: &ConfigOptsServe, + env: Vec<(String, String)>, + ) -> Result { // 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) } } diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs index 1f9a4446c..428670739 100644 --- a/packages/cli/src/server/fullstack/mod.rs +++ b/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 + fn start( + config: &CrateConfig, + serve: &ConfigOptsServe, + env: Vec<(String, String)>, + ) -> Result 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 { + fn rebuild( + &mut self, + crate_config: &CrateConfig, + serve: &ConfigOptsServe, + env: Vec<(String, String)>, + ) -> Result { 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"))??; diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index cb8c15758..3b4563653 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -309,10 +309,19 @@ fn local_path_of_asset(path: &Path) -> Option { } pub(crate) trait Platform { - fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result + fn start( + config: &CrateConfig, + serve: &ConfigOptsServe, + env: Vec<(String, String)>, + ) -> Result where Self: Sized; - fn rebuild(&mut self, config: &CrateConfig) -> Result; + fn rebuild( + &mut self, + config: &CrateConfig, + serve: &ConfigOptsServe, + env: Vec<(String, String)>, + ) -> Result; } fn is_backup_file(path: &Path) -> bool { diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 2b2fd024e..73cf946bc 100644 --- a/packages/cli/src/server/output.rs +++ b/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!(); diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 3b722ede9..9c07a7b55 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/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 { +fn get_ip() -> Option { let socket = match UdpSocket::bind("0.0.0.0:0") { Ok(s) => s, Err(_) => return None, @@ -151,7 +151,7 @@ fn get_ip() -> Option { }; match socket.local_addr() { - Ok(addr) => Some(addr.ip().to_string()), + Ok(addr) => Some(addr.ip()), Err(_) => None, } } diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 241950abd..e8913fc2f 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -156,13 +156,13 @@ impl LaunchBuilder { /// - Any config will be converted into `Some(config)` /// - If the config is for another platform, it will be converted into `None` pub trait TryIntoConfig { - fn into_config(self) -> Option; + fn into_config(self, config: &mut Option); } // A config can always be converted into itself impl TryIntoConfig for Cfg { - fn into_config(self) -> Option { - Some(self) + fn into_config(self, config: &mut Option) { + *config = Some(self); } } @@ -176,15 +176,13 @@ impl TryIntoConfig for Cfg { feature = "fullstack" ))] impl TryIntoConfig for () { - fn into_config(self) -> Option { - None - } + fn into_config(self, config: &mut Option) {} } impl LaunchBuilder { /// Provide a platform-specific config to the builder. pub fn with_cfg(mut self, config: impl TryIntoConfig) -> 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 + for ::dioxus_fullstack::prelude::ServeConfigBuilder + { + fn into_config(self, config: &mut Option) { + 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 for ::dioxus_desktop::Config { - fn into_config(self) -> Option { - None + if_else_cfg! { + if feature = "desktop" { + impl TryIntoConfig for ::dioxus_desktop::Config { + fn into_config(self, config: &mut Option) { + 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 for ::dioxus_mobile::Config { + fn into_config(self, config: &mut Option) { + 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 for ::dioxus_static_site_generation::Config { - fn into_config(self) -> Option { - None - } + fn into_config(self, config: &mut Option) {} } } } @@ -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 for ::dioxus_web::Config { - fn into_config(self) -> Option { - None + if_else_cfg! { + if feature = "fullstack" { + impl TryIntoConfig for ::dioxus_web::Config { + fn into_config(self, config: &mut Option) { + 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 for ::dioxus_web::Config { + fn into_config(self, config: &mut Option) {} + } } } } @@ -290,9 +328,7 @@ mod current_platform { pub use dioxus_liveview::launch::*; } else { impl TryIntoConfig for ::dioxus_liveview::Config { - fn into_config(self) -> Option { - None - } + fn into_config(self, config: &mut Option) {} } } } diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 3e8510b8a..972b7808d 100644 --- a/packages/fullstack/Cargo.toml +++ b/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] diff --git a/packages/fullstack/examples/axum-router/src/main.rs b/packages/fullstack/examples/axum-router/src/main.rs index 95bc3b9b5..d4ef56088 100644 --- a/packages/fullstack/examples/axum-router/src/main.rs +++ b/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 { diff --git a/packages/fullstack/src/config.rs b/packages/fullstack/src/config.rs index f6a497d89..52d805111 100644 --- a/packages/fullstack/src/config.rs +++ b/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) -> 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(); - } - } } diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 1b4626a89..7e667a556 100644 --- a/packages/fullstack/src/launch.rs +++ b/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 + 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 + 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 + Send + Sync>>, + contexts: Vec Box + 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."); + } +} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 6db537a84..582b09d0d 100644 --- a/packages/fullstack/src/lib.rs +++ b/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; diff --git a/packages/playwright-tests/fullstack/src/main.rs b/packages/playwright-tests/fullstack/src/main.rs index 605e25fb7..5438cdc60 100644 --- a/packages/playwright-tests/fullstack/src/main.rs +++ b/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 { diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 1516ea267..43290c12e 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/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, diff --git a/packages/playwright-tests/web.spec.js b/packages/playwright-tests/web.spec.js index aa1a89736..be31defff 100644 --- a/packages/playwright-tests/web.spec.js +++ b/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 \ No newline at end of file +// Shutdown the li