فهرست منبع

Merge pull request #1210 from Demonthos/desktop-serve-cli

Move desktop hot reload into the CLI
Jonathan Kelley 1 سال پیش
والد
کامیت
1ed277154a

+ 2 - 0
packages/cli/Cargo.toml

@@ -81,6 +81,8 @@ rsx-rosetta = { workspace = true }
 dioxus-rsx = { workspace = true }
 dioxus-html = { workspace = true, features = ["hot-reload-context"] }
 dioxus-core = { workspace = true, features = ["serialize"] }
+dioxus-hot-reload = { workspace = true }
+interprocess-docfix = { version = "1.2.2" }
 
 [features]
 default = []

+ 82 - 86
packages/cli/src/builder.rs

@@ -12,7 +12,6 @@ use std::{
     io::Read,
     panic,
     path::PathBuf,
-    process::Command,
     time::Duration,
 };
 use wasm_bindgen_cli_support::Bindgen;
@@ -244,128 +243,125 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
     })
 }
 
-pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
+pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
     log::info!("🚅 Running build [Desktop] command...");
 
+    let t_start = std::time::Instant::now();
     let ignore_files = build_assets(config)?;
 
-    let mut cmd = Command::new("cargo");
-    cmd.current_dir(&config.crate_dir)
+    let mut cmd = subprocess::Exec::cmd("cargo")
+        .cwd(&config.crate_dir)
         .arg("build")
-        .stdout(std::process::Stdio::inherit())
-        .stderr(std::process::Stdio::inherit());
+        .arg("--message-format=json");
 
     if config.release {
-        cmd.arg("--release");
+        cmd = cmd.arg("--release");
     }
     if config.verbose {
-        cmd.arg("--verbose");
+        cmd = cmd.arg("--verbose");
     }
 
     if config.custom_profile.is_some() {
         let custom_profile = config.custom_profile.as_ref().unwrap();
-        cmd.arg("--profile");
-        cmd.arg(custom_profile);
+        cmd = cmd.arg("--profile").arg(custom_profile);
     }
 
     if config.features.is_some() {
         let features_str = config.features.as_ref().unwrap().join(" ");
-        cmd.arg("--features");
-        cmd.arg(features_str);
+        cmd = cmd.arg("--features").arg(features_str);
     }
 
-    match &config.executable {
+    let cmd = match &config.executable {
         crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
         crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
         crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
     };
 
-    let output = cmd.output()?;
+    let warning_messages = prettier_build(cmd)?;
 
-    if !output.status.success() {
-        return Err(Error::BuildFailed("Program build failed.".into()));
-    }
+    let release_type = match config.release {
+        true => "release",
+        false => "debug",
+    };
 
-    if output.status.success() {
-        let release_type = match config.release {
-            true => "release",
-            false => "debug",
-        };
+    let file_name: String;
+    let mut res_path = match &config.executable {
+        crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
+            file_name = name.clone();
+            config.target_dir.join(release_type).join(name)
+        }
+        crate::ExecutableType::Example(name) => {
+            file_name = name.clone();
+            config
+                .target_dir
+                .join(release_type)
+                .join("examples")
+                .join(name)
+        }
+    };
 
-        let file_name: String;
-        let mut res_path = match &config.executable {
-            crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
-                file_name = name.clone();
-                config.target_dir.join(release_type).join(name)
-            }
-            crate::ExecutableType::Example(name) => {
-                file_name = name.clone();
-                config
-                    .target_dir
-                    .join(release_type)
-                    .join("examples")
-                    .join(name)
-            }
-        };
+    let target_file = if cfg!(windows) {
+        res_path.set_extension("exe");
+        format!("{}.exe", &file_name)
+    } else {
+        file_name
+    };
 
-        let target_file = if cfg!(windows) {
-            res_path.set_extension("exe");
-            format!("{}.exe", &file_name)
-        } else {
-            file_name
-        };
+    if !config.out_dir.is_dir() {
+        create_dir_all(&config.out_dir)?;
+    }
+    copy(res_path, &config.out_dir.join(target_file))?;
 
-        if !config.out_dir.is_dir() {
-            create_dir_all(&config.out_dir)?;
-        }
-        copy(res_path, &config.out_dir.join(target_file))?;
-
-        // this code will copy all public file to the output dir
-        if config.asset_dir.is_dir() {
-            let copy_options = fs_extra::dir::CopyOptions {
-                overwrite: true,
-                skip_exist: false,
-                buffer_size: 64000,
-                copy_inside: false,
-                content_only: false,
-                depth: 0,
-            };
+    // this code will copy all public file to the output dir
+    if config.asset_dir.is_dir() {
+        let copy_options = fs_extra::dir::CopyOptions {
+            overwrite: true,
+            skip_exist: false,
+            buffer_size: 64000,
+            copy_inside: false,
+            content_only: false,
+            depth: 0,
+        };
 
-            for entry in std::fs::read_dir(&config.asset_dir)? {
-                let path = entry?.path();
-                if path.is_file() {
-                    std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
-                } else {
-                    match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
-                        Ok(_) => {}
-                        Err(e) => {
-                            log::warn!("Error copying dir: {}", e);
-                        }
+        for entry in std::fs::read_dir(&config.asset_dir)? {
+            let path = entry?.path();
+            if path.is_file() {
+                std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
+            } else {
+                match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
+                    Ok(_) => {}
+                    Err(e) => {
+                        log::warn!("Error copying dir: {}", e);
                     }
-                    for ignore in &ignore_files {
-                        let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
-                        let ignore = config.out_dir.join(ignore);
-                        if ignore.is_file() {
-                            std::fs::remove_file(ignore)?;
-                        }
+                }
+                for ignore in &ignore_files {
+                    let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
+                    let ignore = config.out_dir.join(ignore);
+                    if ignore.is_file() {
+                        std::fs::remove_file(ignore)?;
                     }
                 }
             }
         }
-
-        log::info!(
-            "🚩 Build completed: [./{}]",
-            config
-                .dioxus_config
-                .application
-                .out_dir
-                .clone()
-                .unwrap_or_else(|| PathBuf::from("dist"))
-                .display()
-        );
     }
 
-    Ok(())
+    log::info!(
+        "🚩 Build completed: [./{}]",
+        config
+            .dioxus_config
+            .application
+            .out_dir
+            .clone()
+            .unwrap_or_else(|| PathBuf::from("dist"))
+            .display()
+    );
+
+    println!("build desktop done");
+
+    Ok(BuildResult {
+        warnings: warning_messages,
+        elapsed_time: (t_start - std::time::Instant::now()).as_millis(),
+    })
 }
 
 fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {

+ 8 - 13
packages/cli/src/cli/build.rs

@@ -1,3 +1,4 @@
+use crate::cfg::Platform;
 #[cfg(feature = "plugin")]
 use crate::plugin::PluginManager;
 
@@ -31,27 +32,21 @@ impl Build {
             crate_config.set_features(self.build.features.unwrap());
         }
 
-        let platform = self.build.platform.unwrap_or_else(|| {
-            crate_config
-                .dioxus_config
-                .application
-                .default_platform
-                .clone()
-        });
+        let platform = self
+            .build
+            .platform
+            .unwrap_or(crate_config.dioxus_config.application.default_platform);
 
         #[cfg(feature = "plugin")]
         let _ = PluginManager::on_build_start(&crate_config, &platform);
 
-        match platform.as_str() {
-            "web" => {
+        match platform {
+            Platform::Web => {
                 crate::builder::build(&crate_config, false)?;
             }
-            "desktop" => {
+            Platform::Desktop => {
                 crate::builder::build_desktop(&crate_config, false)?;
             }
-            _ => {
-                return custom_error!("Unsupported platform target.");
-            }
         }
 
         let temp = gen_page(&crate_config.dioxus_config, false);

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

@@ -1,3 +1,6 @@
+use clap::ValueEnum;
+use serde::Serialize;
+
 use super::*;
 
 /// Config options for the build system.
@@ -26,8 +29,8 @@ pub struct ConfigOptsBuild {
     pub profile: Option<String>,
 
     /// Build platform: support Web & Desktop [default: "default_platform"]
-    #[clap(long)]
-    pub platform: Option<String>,
+    #[clap(long, value_enum)]
+    pub platform: Option<Platform>,
 
     /// Space separated list of features to activate
     #[clap(long)]
@@ -69,8 +72,8 @@ pub struct ConfigOptsServe {
     pub profile: Option<String>,
 
     /// Build platform: support Web & Desktop [default: "default_platform"]
-    #[clap(long)]
-    pub platform: Option<String>,
+    #[clap(long, value_enum)]
+    pub platform: Option<Platform>,
 
     /// Build with hot reloading rsx [default: false]
     #[clap(long)]
@@ -88,6 +91,16 @@ pub struct ConfigOptsServe {
     pub features: Option<Vec<String>>,
 }
 
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
+pub enum Platform {
+    #[clap(name = "web")]
+    #[serde(rename = "web")]
+    Web,
+    #[clap(name = "desktop")]
+    #[serde(rename = "desktop")]
+    Desktop,
+}
+
 /// Ensure the given value for `--public-url` is formatted correctly.
 pub fn parse_public_url(val: &str) -> String {
     let prefix = if !val.starts_with('/') { "/" } else { "" };

+ 15 - 37
packages/cli/src/cli/serve.rs

@@ -1,10 +1,5 @@
 use super::*;
-use std::{
-    fs::create_dir_all,
-    io::Write,
-    path::PathBuf,
-    process::{Command, Stdio},
-};
+use std::{fs::create_dir_all, io::Write, path::PathBuf};
 
 /// Run the WASM project on dev-server
 #[derive(Clone, Debug, Parser)]
@@ -39,41 +34,24 @@ impl Serve {
         // Subdirectories don't work with the server
         crate_config.dioxus_config.web.app.base_path = None;
 
-        let platform = self.serve.platform.unwrap_or_else(|| {
-            crate_config
-                .dioxus_config
-                .application
-                .default_platform
-                .clone()
-        });
+        let platform = self
+            .serve
+            .platform
+            .unwrap_or(crate_config.dioxus_config.application.default_platform);
 
-        if platform.as_str() == "desktop" {
-            crate::builder::build_desktop(&crate_config, true)?;
+        match platform {
+            cfg::Platform::Web => {
+                // generate dev-index page
+                Serve::regen_dev_page(&crate_config)?;
 
-            match &crate_config.executable {
-                crate::ExecutableType::Binary(name)
-                | crate::ExecutableType::Lib(name)
-                | crate::ExecutableType::Example(name) => {
-                    let mut file = crate_config.out_dir.join(name);
-                    if cfg!(windows) {
-                        file.set_extension("exe");
-                    }
-                    Command::new(file.to_str().unwrap())
-                        .stdout(Stdio::inherit())
-                        .output()?;
-                }
+                // start the develop server
+                server::web::startup(self.serve.port, crate_config.clone(), self.serve.open)
+                    .await?;
+            }
+            cfg::Platform::Desktop => {
+                server::desktop::startup(crate_config.clone()).await?;
             }
-            return Ok(());
-        } else if platform != "web" {
-            return custom_error!("Unsupported platform target.");
         }
-
-        // generate dev-index page
-        Serve::regen_dev_page(&crate_config)?;
-
-        // start the develop server
-        server::startup(self.serve.port, crate_config.clone(), self.serve.open).await?;
-
         Ok(())
     }
 

+ 3 - 3
packages/cli/src/config.rs

@@ -1,4 +1,4 @@
-use crate::error::Result;
+use crate::{cfg::Platform, error::Result};
 use serde::{Deserialize, Serialize};
 use std::{
     collections::HashMap,
@@ -73,7 +73,7 @@ impl Default for DioxusConfig {
         Self {
             application: ApplicationConfig {
                 name: "dioxus".into(),
-                default_platform: "web".to_string(),
+                default_platform: Platform::Web,
                 out_dir: Some(PathBuf::from("dist")),
                 asset_dir: Some(PathBuf::from("public")),
 
@@ -115,7 +115,7 @@ impl Default for DioxusConfig {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ApplicationConfig {
     pub name: String,
-    pub default_platform: String,
+    pub default_platform: Platform,
     pub out_dir: Option<PathBuf>,
     pub asset_dir: Option<PathBuf>,
 

+ 248 - 0
packages/cli/src/server/desktop/mod.rs

@@ -0,0 +1,248 @@
+use crate::{
+    server::{
+        output::{print_console_info, PrettierOptions},
+        setup_file_watcher, setup_file_watcher_hot_reload,
+    },
+    BuildResult, CrateConfig, Result,
+};
+
+use dioxus_hot_reload::HotReloadMsg;
+use dioxus_html::HtmlCtx;
+use dioxus_rsx::hot_reload::*;
+use interprocess_docfix::local_socket::LocalSocketListener;
+use std::{
+    process::{Child, Command},
+    sync::{Arc, Mutex, RwLock},
+};
+use tokio::sync::broadcast::{self};
+
+#[cfg(feature = "plugin")]
+use plugin::PluginManager;
+
+pub async fn startup(config: CrateConfig) -> Result<()> {
+    // ctrl-c shutdown checker
+    let _crate_config = config.clone();
+    let _ = ctrlc::set_handler(move || {
+        #[cfg(feature = "plugin")]
+        let _ = PluginManager::on_serve_shutdown(&_crate_config);
+        std::process::exit(0);
+    });
+
+    match config.hot_reload {
+        true => serve_hot_reload(config).await?,
+        false => serve_default(config).await?,
+    }
+
+    Ok(())
+}
+
+/// Start the server without hot reload
+pub async fn serve_default(config: CrateConfig) -> Result<()> {
+    let (child, first_build_result) = start_desktop(&config)?;
+    let currently_running_child: RwLock<Child> = RwLock::new(child);
+
+    log::info!("🚀 Starting development server...");
+
+    // We got to own watcher so that it exists for the duration of serve
+    // Otherwise full reload won't work.
+    let _watcher = setup_file_watcher(
+        {
+            let config = config.clone();
+
+            move || {
+                let mut current_child = currently_running_child.write().unwrap();
+                current_child.kill()?;
+                let (child, result) = start_desktop(&config)?;
+                *current_child = child;
+                Ok(result)
+            }
+        },
+        &config,
+        None,
+    )
+    .await?;
+
+    // Print serve info
+    print_console_info(
+        &config,
+        PrettierOptions {
+            changed: vec![],
+            warnings: first_build_result.warnings,
+            elapsed_time: first_build_result.elapsed_time,
+        },
+        None,
+    );
+
+    std::future::pending::<()>().await;
+
+    Ok(())
+}
+
+/// Start the server without hot reload
+
+/// Start dx serve with hot reload
+pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
+    let (_, first_build_result) = start_desktop(&config)?;
+
+    println!("🚀 Starting development server...");
+
+    // Setup hot reload
+    let FileMapBuildResult { map, errors } =
+        FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
+
+    println!("🚀 Starting development server...");
+
+    for err in errors {
+        log::error!("{}", err);
+    }
+
+    let file_map = Arc::new(Mutex::new(map));
+
+    let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100);
+
+    // States
+    // The open interprocess sockets
+    let channels = Arc::new(Mutex::new(Vec::new()));
+
+    // Setup file watcher
+    // We got to own watcher so that it exists for the duration of serve
+    // Otherwise hot reload won't work.
+    let _watcher = setup_file_watcher_hot_reload(
+        &config,
+        hot_reload_tx,
+        file_map.clone(),
+        {
+            let config = config.clone();
+
+            let channels = channels.clone();
+            move || {
+                for channel in &mut *channels.lock().unwrap() {
+                    send_msg(HotReloadMsg::Shutdown, channel);
+                }
+                Ok(start_desktop(&config)?.1)
+            }
+        },
+        None,
+    )
+    .await?;
+
+    // Print serve info
+    print_console_info(
+        &config,
+        PrettierOptions {
+            changed: vec![],
+            warnings: first_build_result.warnings,
+            elapsed_time: first_build_result.elapsed_time,
+        },
+        None,
+    );
+
+    clear_paths();
+
+    match LocalSocketListener::bind("@dioxusin") {
+        Ok(local_socket_stream) => {
+            let aborted = Arc::new(Mutex::new(false));
+
+            // listen for connections
+            std::thread::spawn({
+                let file_map = file_map.clone();
+                let channels = channels.clone();
+                let aborted = aborted.clone();
+                let _ = local_socket_stream.set_nonblocking(true);
+                move || {
+                    loop {
+                        if let Ok(mut connection) = local_socket_stream.accept() {
+                            // send any templates than have changed before the socket connected
+                            let templates: Vec<_> = {
+                                file_map
+                                    .lock()
+                                    .unwrap()
+                                    .map
+                                    .values()
+                                    .filter_map(|(_, template_slot)| *template_slot)
+                                    .collect()
+                            };
+                            for template in templates {
+                                if !send_msg(
+                                    HotReloadMsg::UpdateTemplate(template),
+                                    &mut connection,
+                                ) {
+                                    continue;
+                                }
+                            }
+                            channels.lock().unwrap().push(connection);
+                            println!("Connected to hot reloading 🚀");
+                        }
+                        if *aborted.lock().unwrap() {
+                            break;
+                        }
+                    }
+                }
+            });
+
+            while let Ok(template) = hot_reload_rx.recv().await {
+                let channels = &mut *channels.lock().unwrap();
+                let mut i = 0;
+                while i < channels.len() {
+                    let channel = &mut channels[i];
+                    if send_msg(HotReloadMsg::UpdateTemplate(template), channel) {
+                        i += 1;
+                    } else {
+                        channels.remove(i);
+                    }
+                }
+            }
+        }
+        Err(error) => println!("failed to connect to hot reloading\n{error}"),
+    }
+
+    Ok(())
+}
+
+fn clear_paths() {
+    if cfg!(target_os = "macos") {
+        // On unix, if you force quit the application, it can leave the file socket open
+        // This will cause the local socket listener to fail to open
+        // We check if the file socket is already open from an old session and then delete it
+        let paths = ["./dioxusin", "./@dioxusin"];
+        for path in paths {
+            let path = std::path::PathBuf::from(path);
+            if path.exists() {
+                let _ = std::fs::remove_file(path);
+            }
+        }
+    }
+}
+
+fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
+    if let Ok(msg) = serde_json::to_string(&msg) {
+        if channel.write_all(msg.as_bytes()).is_err() {
+            return false;
+        }
+        if channel.write_all(&[b'\n']).is_err() {
+            return false;
+        }
+        true
+    } else {
+        false
+    }
+}
+
+pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
+    // Run the desktop application
+    let result = crate::builder::build_desktop(config, true)?;
+
+    match &config.executable {
+        crate::ExecutableType::Binary(name)
+        | crate::ExecutableType::Lib(name)
+        | crate::ExecutableType::Example(name) => {
+            let mut file = config.out_dir.join(name);
+            if cfg!(windows) {
+                file.set_extension("exe");
+            }
+            let child = Command::new(file.to_str().unwrap()).spawn()?;
+
+            Ok((child, result))
+        }
+    }
+}

+ 17 - 490
packages/cli/src/server/mod.rs

@@ -1,449 +1,27 @@
-use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result};
-use axum::{
-    body::{Full, HttpBody},
-    extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
-    http::{
-        header::{HeaderName, HeaderValue},
-        Method, Response, StatusCode,
-    },
-    response::IntoResponse,
-    routing::{get, get_service},
-    Router,
-};
-use axum_server::tls_rustls::RustlsConfig;
+use crate::{BuildResult, CrateConfig, Result};
+
 use cargo_metadata::diagnostic::Diagnostic;
 use dioxus_core::Template;
 use dioxus_html::HtmlCtx;
 use dioxus_rsx::hot_reload::*;
 use notify::{RecommendedWatcher, Watcher};
 use std::{
-    net::UdpSocket,
     path::PathBuf,
-    process::Command,
     sync::{Arc, Mutex},
 };
-use tokio::sync::broadcast::{self, Sender};
-use tower::ServiceBuilder;
-use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
-use tower_http::{
-    cors::{Any, CorsLayer},
-    ServiceBuilderExt,
-};
-
-#[cfg(feature = "plugin")]
-use plugin::PluginManager;
-
-mod proxy;
-
-mod hot_reload;
-use hot_reload::*;
+use tokio::sync::broadcast::Sender;
 
 mod output;
 use output::*;
-
-pub struct BuildManager {
-    config: CrateConfig,
-    reload_tx: broadcast::Sender<()>,
-}
-
-impl BuildManager {
-    fn rebuild(&self) -> Result<BuildResult> {
-        log::info!("🪁 Rebuild project");
-        let result = builder::build(&self.config, true)?;
-        // change the websocket reload state to true;
-        // the page will auto-reload.
-        if self
-            .config
-            .dioxus_config
-            .web
-            .watcher
-            .reload_html
-            .unwrap_or(false)
-        {
-            let _ = Serve::regen_dev_page(&self.config);
-        }
-        let _ = self.reload_tx.send(());
-        Ok(result)
-    }
-}
-
-struct WsReloadState {
-    update: broadcast::Sender<()>,
-}
-
-pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
-    // ctrl-c shutdown checker
-    let _crate_config = config.clone();
-    let _ = ctrlc::set_handler(move || {
-        #[cfg(feature = "plugin")]
-        let _ = PluginManager::on_serve_shutdown(&_crate_config);
-        std::process::exit(0);
-    });
-
-    let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
-
-    match config.hot_reload {
-        true => serve_hot_reload(ip, port, config, start_browser).await?,
-        false => serve_default(ip, port, config, start_browser).await?,
-    }
-
-    Ok(())
-}
-
-/// Start the server without hot reload
-pub async fn serve_default(
-    ip: String,
-    port: u16,
-    config: CrateConfig,
-    start_browser: bool,
-) -> Result<()> {
-    let first_build_result = crate::builder::build(&config, false)?;
-
-    log::info!("🚀 Starting development server...");
-
-    // WS Reload Watching
-    let (reload_tx, _) = broadcast::channel(100);
-
-    // We got to own watcher so that it exists for the duration of serve
-    // Otherwise full reload won't work.
-    let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?;
-
-    let ws_reload_state = Arc::new(WsReloadState {
-        update: reload_tx.clone(),
-    });
-
-    // HTTPS
-    // Before console info so it can stop if mkcert isn't installed or fails
-    let rustls_config = get_rustls(&config).await?;
-
-    // Print serve info
-    print_console_info(
-        &ip,
-        port,
-        &config,
-        PrettierOptions {
-            changed: vec![],
-            warnings: first_build_result.warnings,
-            elapsed_time: first_build_result.elapsed_time,
-        },
-    );
-
-    // Router
-    let router = setup_router(config, ws_reload_state, None).await?;
-
-    // Start server
-    start_server(port, router, start_browser, rustls_config).await?;
-
-    Ok(())
-}
-
-/// Start dx serve with hot reload
-pub async fn serve_hot_reload(
-    ip: String,
-    port: u16,
-    config: CrateConfig,
-    start_browser: bool,
-) -> Result<()> {
-    let first_build_result = crate::builder::build(&config, false)?;
-
-    log::info!("🚀 Starting development server...");
-
-    // Setup hot reload
-    let (reload_tx, _) = broadcast::channel(100);
-    let FileMapBuildResult { map, errors } =
-        FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
-
-    for err in errors {
-        log::error!("{}", err);
-    }
-
-    let file_map = Arc::new(Mutex::new(map));
-    let build_manager = Arc::new(BuildManager {
-        config: config.clone(),
-        reload_tx: reload_tx.clone(),
-    });
-
-    let hot_reload_tx = broadcast::channel(100).0;
-
-    // States
-    let hot_reload_state = Arc::new(HotReloadState {
-        messages: hot_reload_tx.clone(),
-        build_manager: build_manager.clone(),
-        file_map: file_map.clone(),
-        watcher_config: config.clone(),
-    });
-
-    let ws_reload_state = Arc::new(WsReloadState {
-        update: reload_tx.clone(),
-    });
-
-    // Setup file watcher
-    // We got to own watcher so that it exists for the duration of serve
-    // Otherwise hot reload won't work.
-    let _watcher = setup_file_watcher_hot_reload(
-        &config,
-        port,
-        ip.clone(),
-        hot_reload_tx,
-        file_map,
-        build_manager,
-    )
-    .await?;
-
-    // HTTPS
-    // Before console info so it can stop if mkcert isn't installed or fails
-    let rustls_config = get_rustls(&config).await?;
-
-    // Print serve info
-    print_console_info(
-        &ip,
-        port,
-        &config,
-        PrettierOptions {
-            changed: vec![],
-            warnings: first_build_result.warnings,
-            elapsed_time: first_build_result.elapsed_time,
-        },
-    );
-
-    // Router
-    let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
-
-    // Start server
-    start_server(port, router, start_browser, rustls_config).await?;
-
-    Ok(())
-}
-
-const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
-const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
-
-/// Returns an enum of rustls config and a bool if mkcert isn't installed
-async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
-    let web_config = &config.dioxus_config.web.https;
-    if web_config.enabled != Some(true) {
-        return Ok(None);
-    }
-
-    let (cert_path, key_path) = match web_config.mkcert {
-        // mkcert, use it
-        Some(true) => {
-            // Get paths to store certs, otherwise use ssl/item.pem
-            let key_path = web_config
-                .key_path
-                .clone()
-                .unwrap_or(DEFAULT_KEY_PATH.to_string());
-
-            let cert_path = web_config
-                .cert_path
-                .clone()
-                .unwrap_or(DEFAULT_CERT_PATH.to_string());
-
-            // Create ssl directory if using defaults
-            if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
-                _ = fs::create_dir("ssl");
-            }
-
-            let cmd = Command::new("mkcert")
-                .args([
-                    "-install",
-                    "-key-file",
-                    &key_path,
-                    "-cert-file",
-                    &cert_path,
-                    "localhost",
-                    "::1",
-                    "127.0.0.1",
-                ])
-                .spawn();
-
-            match cmd {
-                Err(e) => {
-                    match e.kind() {
-                        io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
-                        e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
-                    };
-                    return Err("failed to generate mkcert certificates".into());
-                }
-                Ok(mut cmd) => {
-                    cmd.wait()?;
-                }
-            }
-
-            (cert_path, key_path)
-        }
-        // not mkcert
-        Some(false) => {
-            // get paths to cert & key
-            if let (Some(key), Some(cert)) =
-                (web_config.key_path.clone(), web_config.cert_path.clone())
-            {
-                (cert, key)
-            } else {
-                // missing cert or key
-                return Err("https is enabled but cert or key path is missing".into());
-            }
-        }
-        // other
-        _ => return Ok(None),
-    };
-
-    Ok(Some(
-        RustlsConfig::from_pem_file(cert_path, key_path).await?,
-    ))
-}
-
-/// Sets up and returns a router
-async fn setup_router(
-    config: CrateConfig,
-    ws_reload: Arc<WsReloadState>,
-    hot_reload: Option<Arc<HotReloadState>>,
-) -> Result<Router> {
-    // Setup cors
-    let cors = CorsLayer::new()
-        // allow `GET` and `POST` when accessing the resource
-        .allow_methods([Method::GET, Method::POST])
-        // allow requests from any origin
-        .allow_origin(Any)
-        .allow_headers(Any);
-
-    let (coep, coop) = if config.cross_origin_policy {
-        (
-            HeaderValue::from_static("require-corp"),
-            HeaderValue::from_static("same-origin"),
-        )
-    } else {
-        (
-            HeaderValue::from_static("unsafe-none"),
-            HeaderValue::from_static("unsafe-none"),
-        )
-    };
-
-    // Create file service
-    let file_service_config = config.clone();
-    let file_service = ServiceBuilder::new()
-        .override_response_header(
-            HeaderName::from_static("cross-origin-embedder-policy"),
-            coep,
-        )
-        .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
-        .and_then(
-            move |response: Response<ServeFileSystemResponseBody>| async move {
-                let response = if file_service_config
-                    .dioxus_config
-                    .web
-                    .watcher
-                    .index_on_404
-                    .unwrap_or(false)
-                    && response.status() == StatusCode::NOT_FOUND
-                {
-                    let body = Full::from(
-                        // TODO: Cache/memoize this.
-                        std::fs::read_to_string(
-                            file_service_config
-                                .crate_dir
-                                .join(file_service_config.out_dir)
-                                .join("index.html"),
-                        )
-                        .ok()
-                        .unwrap(),
-                    )
-                    .map_err(|err| match err {})
-                    .boxed();
-                    Response::builder()
-                        .status(StatusCode::OK)
-                        .body(body)
-                        .unwrap()
-                } else {
-                    response.map(|body| body.boxed())
-                };
-                Ok(response)
-            },
-        )
-        .service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
-
-    // Setup websocket
-    let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
-
-    // Setup proxy
-    for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
-        router = proxy::add_proxy(router, &proxy_config)?;
-    }
-
-    // Route file service
-    router = router.fallback(get_service(file_service).handle_error(
-        |error: std::io::Error| async move {
-            (
-                StatusCode::INTERNAL_SERVER_ERROR,
-                format!("Unhandled internal error: {}", error),
-            )
-        },
-    ));
-
-    // Setup routes
-    router = router
-        .route("/_dioxus/hot_reload", get(hot_reload_handler))
-        .layer(cors)
-        .layer(Extension(ws_reload));
-
-    if let Some(hot_reload) = hot_reload {
-        router = router.layer(Extension(hot_reload))
-    }
-
-    Ok(router)
-}
-
-/// Starts dx serve with no hot reload
-async fn start_server(
-    port: u16,
-    router: Router,
-    start_browser: bool,
-    rustls: Option<RustlsConfig>,
-) -> Result<()> {
-    // If plugins, call on_serve_start event
-    #[cfg(feature = "plugin")]
-    PluginManager::on_serve_start(&config)?;
-
-    // Parse address
-    let addr = format!("0.0.0.0:{}", port).parse().unwrap();
-
-    // Open the browser
-    if start_browser {
-        match rustls {
-            Some(_) => _ = open::that(format!("https://{}", addr)),
-            None => _ = open::that(format!("http://{}", addr)),
-        }
-    }
-
-    // Start the server with or without rustls
-    match rustls {
-        Some(rustls) => {
-            axum_server::bind_rustls(addr, rustls)
-                .serve(router.into_make_service())
-                .await?
-        }
-        None => {
-            axum::Server::bind(&addr)
-                .serve(router.into_make_service())
-                .await?
-        }
-    }
-
-    Ok(())
-}
+pub mod desktop;
+pub mod web;
 
 /// Sets up a file watcher
-async fn setup_file_watcher(
+async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
+    build_with: F,
     config: &CrateConfig,
-    port: u16,
-    watcher_ip: String,
-    reload_tx: Sender<()>,
+    web_info: Option<WebServerInfo>,
 ) -> Result<RecommendedWatcher> {
-    let build_manager = BuildManager {
-        config: config.clone(),
-        reload_tx,
-    };
-
     let mut last_update_time = chrono::Local::now().timestamp();
 
     // file watcher: check file change
@@ -460,20 +38,19 @@ async fn setup_file_watcher(
         let config = watcher_config.clone();
         if let Ok(e) = info {
             if chrono::Local::now().timestamp() > last_update_time {
-                match build_manager.rebuild() {
+                match build_with() {
                     Ok(res) => {
                         last_update_time = chrono::Local::now().timestamp();
 
                         #[allow(clippy::redundant_clone)]
                         print_console_info(
-                            &watcher_ip,
-                            port,
                             &config,
                             PrettierOptions {
                                 changed: e.paths.clone(),
                                 warnings: res.warnings,
                                 elapsed_time: res.elapsed_time,
                             },
+                            web_info.clone(),
                         );
 
                         #[cfg(feature = "plugin")]
@@ -502,13 +79,12 @@ async fn setup_file_watcher(
 
 // Todo: reduce duplication and merge with setup_file_watcher()
 /// Sets up a file watcher with hot reload
-async fn setup_file_watcher_hot_reload(
+async fn setup_file_watcher_hot_reload<F: Fn() -> Result<BuildResult> + Send + 'static>(
     config: &CrateConfig,
-    port: u16,
-    watcher_ip: String,
     hot_reload_tx: Sender<Template<'static>>,
     file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
-    build_manager: Arc<BuildManager>,
+    build_with: F,
+    web_info: Option<WebServerInfo>,
 ) -> Result<RecommendedWatcher> {
     // file watcher: check file change
     let allow_watch_path = config
@@ -533,17 +109,16 @@ async fn setup_file_watcher_hot_reload(
                     for path in evt.paths.clone() {
                         // if this is not a rust file, rebuild the whole project
                         if path.extension().and_then(|p| p.to_str()) != Some("rs") {
-                            match build_manager.rebuild() {
+                            match build_with() {
                                 Ok(res) => {
                                     print_console_info(
-                                        &watcher_ip,
-                                        port,
                                         &config,
                                         PrettierOptions {
                                             changed: evt.paths,
                                             warnings: res.warnings,
                                             elapsed_time: res.elapsed_time,
                                         },
+                                        web_info.clone(),
                                     );
                                 }
                                 Err(err) => {
@@ -560,17 +135,16 @@ async fn setup_file_watcher_hot_reload(
                                 messages.extend(msgs);
                             }
                             Ok(UpdateResult::NeedsRebuild) => {
-                                match build_manager.rebuild() {
+                                match build_with() {
                                     Ok(res) => {
                                         print_console_info(
-                                            &watcher_ip,
-                                            port,
                                             &config,
                                             PrettierOptions {
                                                 changed: evt.paths,
                                                 warnings: res.warnings,
                                                 elapsed_time: res.elapsed_time,
                                             },
+                                            web_info.clone(),
                                         );
                                     }
                                     Err(err) => {
@@ -606,50 +180,3 @@ async fn setup_file_watcher_hot_reload(
 
     Ok(watcher)
 }
-
-/// Get the network ip
-fn get_ip() -> Option<String> {
-    let socket = match UdpSocket::bind("0.0.0.0:0") {
-        Ok(s) => s,
-        Err(_) => return None,
-    };
-
-    match socket.connect("8.8.8.8:80") {
-        Ok(()) => (),
-        Err(_) => return None,
-    };
-
-    match socket.local_addr() {
-        Ok(addr) => Some(addr.ip().to_string()),
-        Err(_) => None,
-    }
-}
-
-/// Handle websockets
-async fn ws_handler(
-    ws: WebSocketUpgrade,
-    _: Option<TypedHeader<headers::UserAgent>>,
-    Extension(state): Extension<Arc<WsReloadState>>,
-) -> impl IntoResponse {
-    ws.on_upgrade(|mut socket| async move {
-        let mut rx = state.update.subscribe();
-        let reload_watcher = tokio::spawn(async move {
-            loop {
-                rx.recv().await.unwrap();
-                // ignore the error
-                if socket
-                    .send(Message::Text(String::from("reload")))
-                    .await
-                    .is_err()
-                {
-                    break;
-                }
-
-                // flush the errors after recompling
-                rx = rx.resubscribe();
-            }
-        });
-
-        reload_watcher.await.unwrap();
-    })
-}

+ 33 - 21
packages/cli/src/server/output.rs

@@ -11,7 +11,17 @@ pub struct PrettierOptions {
     pub elapsed_time: u128,
 }
 
-pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) {
+#[derive(Debug, Clone)]
+pub struct WebServerInfo {
+    pub ip: String,
+    pub port: u16,
+}
+
+pub fn print_console_info(
+    config: &CrateConfig,
+    options: PrettierOptions,
+    web_info: Option<WebServerInfo>,
+) {
     if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
         "cls"
     } else {
@@ -70,26 +80,28 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options:
         );
     }
 
-    if config.dioxus_config.web.https.enabled == Some(true) {
-        println!(
-            "\t> Local : {}",
-            format!("https://localhost:{}/", port).blue()
-        );
-        println!(
-            "\t> Network : {}",
-            format!("https://{}:{}/", ip, port).blue()
-        );
-        println!("\t> HTTPS : {}", "Enabled".to_string().green());
-    } else {
-        println!(
-            "\t> Local : {}",
-            format!("http://localhost:{}/", port).blue()
-        );
-        println!(
-            "\t> Network : {}",
-            format!("http://{}:{}/", ip, port).blue()
-        );
-        println!("\t> HTTPS : {}", "Disabled".to_string().red());
+    if let Some(WebServerInfo { ip, port }) = web_info {
+        if config.dioxus_config.web.https.enabled == Some(true) {
+            println!(
+                "\t> Local : {}",
+                format!("https://localhost:{}/", port).blue()
+            );
+            println!(
+                "\t> Network : {}",
+                format!("https://{}:{}/", ip, port).blue()
+            );
+            println!("\t> HTTPS : {}", "Enabled".to_string().green());
+        } else {
+            println!(
+                "\t> Local : {}",
+                format!("http://localhost:{}/", port).blue()
+            );
+            println!(
+                "\t> Network : {}",
+                format!("http://{}:{}/", ip, port).blue()
+            );
+            println!("\t> HTTPS : {}", "Disabled".to_string().red());
+        }
     }
     println!();
     println!("\t> Profile : {}", profile.green());

+ 0 - 2
packages/cli/src/server/hot_reload.rs → packages/cli/src/server/web/hot_reload.rs

@@ -10,12 +10,10 @@ use dioxus_html::HtmlCtx;
 use dioxus_rsx::hot_reload::FileMap;
 use tokio::sync::broadcast;
 
-use super::BuildManager;
 use crate::CrateConfig;
 
 pub struct HotReloadState {
     pub messages: broadcast::Sender<Template<'static>>,
-    pub build_manager: Arc<BuildManager>,
     pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
     pub watcher_config: CrateConfig,
 }

+ 490 - 0
packages/cli/src/server/web/mod.rs

@@ -0,0 +1,490 @@
+use crate::{
+    builder,
+    serve::Serve,
+    server::{
+        output::{print_console_info, PrettierOptions, WebServerInfo},
+        setup_file_watcher, setup_file_watcher_hot_reload,
+    },
+    BuildResult, CrateConfig, Result,
+};
+use axum::{
+    body::{Full, HttpBody},
+    extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
+    http::{
+        header::{HeaderName, HeaderValue},
+        Method, Response, StatusCode,
+    },
+    response::IntoResponse,
+    routing::{get, get_service},
+    Router,
+};
+use axum_server::tls_rustls::RustlsConfig;
+
+use dioxus_html::HtmlCtx;
+use dioxus_rsx::hot_reload::*;
+use std::{
+    net::UdpSocket,
+    process::Command,
+    sync::{Arc, Mutex},
+};
+use tokio::sync::broadcast::{self, Sender};
+use tower::ServiceBuilder;
+use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
+use tower_http::{
+    cors::{Any, CorsLayer},
+    ServiceBuilderExt,
+};
+
+#[cfg(feature = "plugin")]
+use plugin::PluginManager;
+
+mod proxy;
+
+mod hot_reload;
+use hot_reload::*;
+
+struct WsReloadState {
+    update: broadcast::Sender<()>,
+}
+
+pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
+    // ctrl-c shutdown checker
+    let _crate_config = config.clone();
+    let _ = ctrlc::set_handler(move || {
+        #[cfg(feature = "plugin")]
+        let _ = PluginManager::on_serve_shutdown(&_crate_config);
+        std::process::exit(0);
+    });
+
+    let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
+
+    match config.hot_reload {
+        true => serve_hot_reload(ip, port, config, start_browser).await?,
+        false => serve_default(ip, port, config, start_browser).await?,
+    }
+
+    Ok(())
+}
+
+/// Start the server without hot reload
+pub async fn serve_default(
+    ip: String,
+    port: u16,
+    config: CrateConfig,
+    start_browser: bool,
+) -> Result<()> {
+    let first_build_result = crate::builder::build(&config, false)?;
+
+    log::info!("🚀 Starting development server...");
+
+    // WS Reload Watching
+    let (reload_tx, _) = broadcast::channel(100);
+
+    // We got to own watcher so that it exists for the duration of serve
+    // Otherwise full reload won't work.
+    let _watcher = setup_file_watcher(
+        {
+            let config = config.clone();
+            let reload_tx = reload_tx.clone();
+            move || build(&config, &reload_tx)
+        },
+        &config,
+        Some(WebServerInfo {
+            ip: ip.clone(),
+            port,
+        }),
+    )
+    .await?;
+
+    let ws_reload_state = Arc::new(WsReloadState {
+        update: reload_tx.clone(),
+    });
+
+    // HTTPS
+    // Before console info so it can stop if mkcert isn't installed or fails
+    let rustls_config = get_rustls(&config).await?;
+
+    // Print serve info
+    print_console_info(
+        &config,
+        PrettierOptions {
+            changed: vec![],
+            warnings: first_build_result.warnings,
+            elapsed_time: first_build_result.elapsed_time,
+        },
+        Some(crate::server::output::WebServerInfo {
+            ip: ip.clone(),
+            port,
+        }),
+    );
+
+    // Router
+    let router = setup_router(config, ws_reload_state, None).await?;
+
+    // Start server
+    start_server(port, router, start_browser, rustls_config).await?;
+
+    Ok(())
+}
+
+/// Start dx serve with hot reload
+pub async fn serve_hot_reload(
+    ip: String,
+    port: u16,
+    config: CrateConfig,
+    start_browser: bool,
+) -> Result<()> {
+    let first_build_result = crate::builder::build(&config, false)?;
+
+    log::info!("🚀 Starting development server...");
+
+    // Setup hot reload
+    let (reload_tx, _) = broadcast::channel(100);
+    let FileMapBuildResult { map, errors } =
+        FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
+
+    for err in errors {
+        log::error!("{}", err);
+    }
+
+    let file_map = Arc::new(Mutex::new(map));
+
+    let hot_reload_tx = broadcast::channel(100).0;
+
+    // States
+    let hot_reload_state = Arc::new(HotReloadState {
+        messages: hot_reload_tx.clone(),
+        file_map: file_map.clone(),
+        watcher_config: config.clone(),
+    });
+
+    let ws_reload_state = Arc::new(WsReloadState {
+        update: reload_tx.clone(),
+    });
+
+    // Setup file watcher
+    // We got to own watcher so that it exists for the duration of serve
+    // Otherwise hot reload won't work.
+    let _watcher = setup_file_watcher_hot_reload(
+        &config,
+        hot_reload_tx,
+        file_map,
+        {
+            let config = config.clone();
+            let reload_tx = reload_tx.clone();
+            move || build(&config, &reload_tx)
+        },
+        Some(WebServerInfo {
+            ip: ip.clone(),
+            port,
+        }),
+    )
+    .await?;
+
+    // HTTPS
+    // Before console info so it can stop if mkcert isn't installed or fails
+    let rustls_config = get_rustls(&config).await?;
+
+    // Print serve info
+    print_console_info(
+        &config,
+        PrettierOptions {
+            changed: vec![],
+            warnings: first_build_result.warnings,
+            elapsed_time: first_build_result.elapsed_time,
+        },
+        Some(WebServerInfo {
+            ip: ip.clone(),
+            port,
+        }),
+    );
+
+    // Router
+    let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
+
+    // Start server
+    start_server(port, router, start_browser, rustls_config).await?;
+
+    Ok(())
+}
+
+const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
+const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
+
+/// Returns an enum of rustls config and a bool if mkcert isn't installed
+async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
+    let web_config = &config.dioxus_config.web.https;
+    if web_config.enabled != Some(true) {
+        return Ok(None);
+    }
+
+    let (cert_path, key_path) = match web_config.mkcert {
+        // mkcert, use it
+        Some(true) => {
+            // Get paths to store certs, otherwise use ssl/item.pem
+            let key_path = web_config
+                .key_path
+                .clone()
+                .unwrap_or(DEFAULT_KEY_PATH.to_string());
+
+            let cert_path = web_config
+                .cert_path
+                .clone()
+                .unwrap_or(DEFAULT_CERT_PATH.to_string());
+
+            // Create ssl directory if using defaults
+            if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
+                _ = fs::create_dir("ssl");
+            }
+
+            let cmd = Command::new("mkcert")
+                .args([
+                    "-install",
+                    "-key-file",
+                    &key_path,
+                    "-cert-file",
+                    &cert_path,
+                    "localhost",
+                    "::1",
+                    "127.0.0.1",
+                ])
+                .spawn();
+
+            match cmd {
+                Err(e) => {
+                    match e.kind() {
+                        io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
+                        e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
+                    };
+                    return Err("failed to generate mkcert certificates".into());
+                }
+                Ok(mut cmd) => {
+                    cmd.wait()?;
+                }
+            }
+
+            (cert_path, key_path)
+        }
+        // not mkcert
+        Some(false) => {
+            // get paths to cert & key
+            if let (Some(key), Some(cert)) =
+                (web_config.key_path.clone(), web_config.cert_path.clone())
+            {
+                (cert, key)
+            } else {
+                // missing cert or key
+                return Err("https is enabled but cert or key path is missing".into());
+            }
+        }
+        // other
+        _ => return Ok(None),
+    };
+
+    Ok(Some(
+        RustlsConfig::from_pem_file(cert_path, key_path).await?,
+    ))
+}
+
+/// Sets up and returns a router
+async fn setup_router(
+    config: CrateConfig,
+    ws_reload: Arc<WsReloadState>,
+    hot_reload: Option<Arc<HotReloadState>>,
+) -> Result<Router> {
+    // Setup cors
+    let cors = CorsLayer::new()
+        // allow `GET` and `POST` when accessing the resource
+        .allow_methods([Method::GET, Method::POST])
+        // allow requests from any origin
+        .allow_origin(Any)
+        .allow_headers(Any);
+
+    let (coep, coop) = if config.cross_origin_policy {
+        (
+            HeaderValue::from_static("require-corp"),
+            HeaderValue::from_static("same-origin"),
+        )
+    } else {
+        (
+            HeaderValue::from_static("unsafe-none"),
+            HeaderValue::from_static("unsafe-none"),
+        )
+    };
+
+    // Create file service
+    let file_service_config = config.clone();
+    let file_service = ServiceBuilder::new()
+        .override_response_header(
+            HeaderName::from_static("cross-origin-embedder-policy"),
+            coep,
+        )
+        .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
+        .and_then(
+            move |response: Response<ServeFileSystemResponseBody>| async move {
+                let response = if file_service_config
+                    .dioxus_config
+                    .web
+                    .watcher
+                    .index_on_404
+                    .unwrap_or(false)
+                    && response.status() == StatusCode::NOT_FOUND
+                {
+                    let body = Full::from(
+                        // TODO: Cache/memoize this.
+                        std::fs::read_to_string(
+                            file_service_config
+                                .crate_dir
+                                .join(file_service_config.out_dir)
+                                .join("index.html"),
+                        )
+                        .ok()
+                        .unwrap(),
+                    )
+                    .map_err(|err| match err {})
+                    .boxed();
+                    Response::builder()
+                        .status(StatusCode::OK)
+                        .body(body)
+                        .unwrap()
+                } else {
+                    response.map(|body| body.boxed())
+                };
+                Ok(response)
+            },
+        )
+        .service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
+
+    // Setup websocket
+    let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
+
+    // Setup proxy
+    for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
+        router = proxy::add_proxy(router, &proxy_config)?;
+    }
+
+    // Route file service
+    router = router.fallback(get_service(file_service).handle_error(
+        |error: std::io::Error| async move {
+            (
+                StatusCode::INTERNAL_SERVER_ERROR,
+                format!("Unhandled internal error: {}", error),
+            )
+        },
+    ));
+
+    // Setup routes
+    router = router
+        .route("/_dioxus/hot_reload", get(hot_reload_handler))
+        .layer(cors)
+        .layer(Extension(ws_reload));
+
+    if let Some(hot_reload) = hot_reload {
+        router = router.layer(Extension(hot_reload))
+    }
+
+    Ok(router)
+}
+
+/// Starts dx serve with no hot reload
+async fn start_server(
+    port: u16,
+    router: Router,
+    start_browser: bool,
+    rustls: Option<RustlsConfig>,
+) -> Result<()> {
+    // If plugins, call on_serve_start event
+    #[cfg(feature = "plugin")]
+    PluginManager::on_serve_start(&config)?;
+
+    // Parse address
+    let addr = format!("0.0.0.0:{}", port).parse().unwrap();
+
+    // Open the browser
+    if start_browser {
+        match rustls {
+            Some(_) => _ = open::that(format!("https://{}", addr)),
+            None => _ = open::that(format!("http://{}", addr)),
+        }
+    }
+
+    // Start the server with or without rustls
+    match rustls {
+        Some(rustls) => {
+            axum_server::bind_rustls(addr, rustls)
+                .serve(router.into_make_service())
+                .await?
+        }
+        None => {
+            axum::Server::bind(&addr)
+                .serve(router.into_make_service())
+                .await?
+        }
+    }
+
+    Ok(())
+}
+
+/// Get the network ip
+fn get_ip() -> Option<String> {
+    let socket = match UdpSocket::bind("0.0.0.0:0") {
+        Ok(s) => s,
+        Err(_) => return None,
+    };
+
+    match socket.connect("8.8.8.8:80") {
+        Ok(()) => (),
+        Err(_) => return None,
+    };
+
+    match socket.local_addr() {
+        Ok(addr) => Some(addr.ip().to_string()),
+        Err(_) => None,
+    }
+}
+
+/// Handle websockets
+async fn ws_handler(
+    ws: WebSocketUpgrade,
+    _: Option<TypedHeader<headers::UserAgent>>,
+    Extension(state): Extension<Arc<WsReloadState>>,
+) -> impl IntoResponse {
+    ws.on_upgrade(|mut socket| async move {
+        let mut rx = state.update.subscribe();
+        let reload_watcher = tokio::spawn(async move {
+            loop {
+                rx.recv().await.unwrap();
+                // ignore the error
+                if socket
+                    .send(Message::Text(String::from("reload")))
+                    .await
+                    .is_err()
+                {
+                    break;
+                }
+
+                // flush the errors after recompling
+                rx = rx.resubscribe();
+            }
+        });
+
+        reload_watcher.await.unwrap();
+    })
+}
+
+fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
+    let result = builder::build(config, true)?;
+    // change the websocket reload state to true;
+    // the page will auto-reload.
+    if config
+        .dioxus_config
+        .web
+        .watcher
+        .reload_html
+        .unwrap_or(false)
+    {
+        let _ = Serve::regen_dev_page(config);
+    }
+    let _ = reload_tx.send(());
+    Ok(result)
+}

+ 0 - 0
packages/cli/src/server/proxy.rs → packages/cli/src/server/web/proxy.rs