Merge pull request #1210 from Demonthos/desktop-serve-cli
Move desktop hot reload into the CLI
This commit is contained in:
12 files changed
+916
-657
No files matched your search
@@ -81,6 +81,8 @@ rsx-rosetta = { workspace = true }
|
|||||||
dioxus-rsx = { workspace = true }
|
dioxus-rsx = { workspace = true }
|
||||||
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
||||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||||
|
dioxus-hot-reload = { workspace = true }
|
||||||
|
interprocess-docfix = { version = "1.2.2" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
+83
-87
@@ -12,7 +12,6 @@ use std::{
|
|||||||
io::Read,
|
io::Read,
|
||||||
panic,
|
panic,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::Command,
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use wasm_bindgen_cli_support::Bindgen;
|
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...");
|
log::info!("🚅 Running build [Desktop] command...");
|
||||||
|
|
||||||
|
let t_start = std::time::Instant::now();
|
||||||
let ignore_files = build_assets(config)?;
|
let ignore_files = build_assets(config)?;
|
||||||
|
|
||||||
let mut cmd = Command::new("cargo");
|
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||||
cmd.current_dir(&config.crate_dir)
|
.cwd(&config.crate_dir)
|
||||||
.arg("build")
|
.arg("build")
|
||||||
.stdout(std::process::Stdio::inherit())
|
.arg("--message-format=json");
|
||||||
.stderr(std::process::Stdio::inherit());
|
|
||||||
|
|
||||||
if config.release {
|
if config.release {
|
||||||
cmd.arg("--release");
|
cmd = cmd.arg("--release");
|
||||||
}
|
}
|
||||||
if config.verbose {
|
if config.verbose {
|
||||||
cmd.arg("--verbose");
|
cmd = cmd.arg("--verbose");
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.custom_profile.is_some() {
|
if config.custom_profile.is_some() {
|
||||||
let custom_profile = config.custom_profile.as_ref().unwrap();
|
let custom_profile = config.custom_profile.as_ref().unwrap();
|
||||||
cmd.arg("--profile");
|
cmd = cmd.arg("--profile").arg(custom_profile);
|
||||||
cmd.arg(custom_profile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.features.is_some() {
|
if config.features.is_some() {
|
||||||
let features_str = config.features.as_ref().unwrap().join(" ");
|
let features_str = config.features.as_ref().unwrap().join(" ");
|
||||||
cmd.arg("--features");
|
cmd = cmd.arg("--features").arg(features_str);
|
||||||
cmd.arg(features_str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match &config.executable {
|
let cmd = match &config.executable {
|
||||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||||
crate::ExecutableType::Example(name) => cmd.arg("--example").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() {
|
let release_type = match config.release {
|
||||||
return Err(Error::BuildFailed("Program build failed.".into()));
|
true => "release",
|
||||||
}
|
false => "debug",
|
||||||
|
};
|
||||||
|
|
||||||
if output.status.success() {
|
let file_name: String;
|
||||||
let release_type = match config.release {
|
let mut res_path = match &config.executable {
|
||||||
true => "release",
|
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||||
false => "debug",
|
file_name = name.clone();
|
||||||
};
|
config.target_dir.join(release_type).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
|
|
||||||
};
|
|
||||||
|
|
||||||
if !config.out_dir.is_dir() {
|
|
||||||
create_dir_all(&config.out_dir)?;
|
|
||||||
}
|
}
|
||||||
copy(res_path, &config.out_dir.join(target_file))?;
|
crate::ExecutableType::Example(name) => {
|
||||||
|
file_name = name.clone();
|
||||||
|
config
|
||||||
|
.target_dir
|
||||||
|
.join(release_type)
|
||||||
|
.join("examples")
|
||||||
|
.join(name)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// this code will copy all public file to the output dir
|
let target_file = if cfg!(windows) {
|
||||||
if config.asset_dir.is_dir() {
|
res_path.set_extension("exe");
|
||||||
let copy_options = fs_extra::dir::CopyOptions {
|
format!("{}.exe", &file_name)
|
||||||
overwrite: true,
|
} else {
|
||||||
skip_exist: false,
|
file_name
|
||||||
buffer_size: 64000,
|
};
|
||||||
copy_inside: false,
|
|
||||||
content_only: false,
|
|
||||||
depth: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for entry in std::fs::read_dir(&config.asset_dir)? {
|
if !config.out_dir.is_dir() {
|
||||||
let path = entry?.path();
|
create_dir_all(&config.out_dir)?;
|
||||||
if path.is_file() {
|
}
|
||||||
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
|
copy(res_path, &config.out_dir.join(target_file))?;
|
||||||
} else {
|
|
||||||
match fs_extra::dir::copy(&path, &config.out_dir, ©_options) {
|
// this code will copy all public file to the output dir
|
||||||
Ok(_) => {}
|
if config.asset_dir.is_dir() {
|
||||||
Err(e) => {
|
let copy_options = fs_extra::dir::CopyOptions {
|
||||||
log::warn!("Error copying dir: {}", e);
|
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, ©_options) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Error copying dir: {}", e);
|
||||||
}
|
}
|
||||||
for ignore in &ignore_files {
|
}
|
||||||
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
|
for ignore in &ignore_files {
|
||||||
let ignore = config.out_dir.join(ignore);
|
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
|
||||||
if ignore.is_file() {
|
let ignore = config.out_dir.join(ignore);
|
||||||
std::fs::remove_file(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>> {
|
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::cfg::Platform;
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
use crate::plugin::PluginManager;
|
use crate::plugin::PluginManager;
|
||||||
|
|
||||||
@@ -31,27 +32,21 @@ impl Build {
|
|||||||
crate_config.set_features(self.build.features.unwrap());
|
crate_config.set_features(self.build.features.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
let platform = self.build.platform.unwrap_or_else(|| {
|
let platform = self
|
||||||
crate_config
|
.build
|
||||||
.dioxus_config
|
.platform
|
||||||
.application
|
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||||
.default_platform
|
|
||||||
.clone()
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||||
|
|
||||||
match platform.as_str() {
|
match platform {
|
||||||
"web" => {
|
Platform::Web => {
|
||||||
crate::builder::build(&crate_config, false)?;
|
crate::builder::build(&crate_config, false)?;
|
||||||
}
|
}
|
||||||
"desktop" => {
|
Platform::Desktop => {
|
||||||
crate::builder::build_desktop(&crate_config, false)?;
|
crate::builder::build_desktop(&crate_config, false)?;
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
return custom_error!("Unsupported platform target.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let temp = gen_page(&crate_config.dioxus_config, false);
|
let temp = gen_page(&crate_config.dioxus_config, false);
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Config options for the build system.
|
/// Config options for the build system.
|
||||||
@@ -26,8 +29,8 @@ pub struct ConfigOptsBuild {
|
|||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
|
|
||||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||||
#[clap(long)]
|
#[clap(long, value_enum)]
|
||||||
pub platform: Option<String>,
|
pub platform: Option<Platform>,
|
||||||
|
|
||||||
/// Space separated list of features to activate
|
/// Space separated list of features to activate
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
@@ -69,8 +72,8 @@ pub struct ConfigOptsServe {
|
|||||||
pub profile: Option<String>,
|
pub profile: Option<String>,
|
||||||
|
|
||||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||||
#[clap(long)]
|
#[clap(long, value_enum)]
|
||||||
pub platform: Option<String>,
|
pub platform: Option<Platform>,
|
||||||
|
|
||||||
/// Build with hot reloading rsx [default: false]
|
/// Build with hot reloading rsx [default: false]
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
@@ -88,6 +91,16 @@ pub struct ConfigOptsServe {
|
|||||||
pub features: Option<Vec<String>>,
|
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.
|
/// Ensure the given value for `--public-url` is formatted correctly.
|
||||||
pub fn parse_public_url(val: &str) -> String {
|
pub fn parse_public_url(val: &str) -> String {
|
||||||
let prefix = if !val.starts_with('/') { "/" } else { "" };
|
let prefix = if !val.starts_with('/') { "/" } else { "" };
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::{
|
use std::{fs::create_dir_all, io::Write, path::PathBuf};
|
||||||
fs::create_dir_all,
|
|
||||||
io::Write,
|
|
||||||
path::PathBuf,
|
|
||||||
process::{Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Run the WASM project on dev-server
|
/// Run the WASM project on dev-server
|
||||||
#[derive(Clone, Debug, Parser)]
|
#[derive(Clone, Debug, Parser)]
|
||||||
@@ -39,41 +34,24 @@ impl Serve {
|
|||||||
// Subdirectories don't work with the server
|
// Subdirectories don't work with the server
|
||||||
crate_config.dioxus_config.web.app.base_path = None;
|
crate_config.dioxus_config.web.app.base_path = None;
|
||||||
|
|
||||||
let platform = self.serve.platform.unwrap_or_else(|| {
|
let platform = self
|
||||||
crate_config
|
.serve
|
||||||
.dioxus_config
|
.platform
|
||||||
.application
|
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||||
.default_platform
|
|
||||||
.clone()
|
|
||||||
});
|
|
||||||
|
|
||||||
if platform.as_str() == "desktop" {
|
match platform {
|
||||||
crate::builder::build_desktop(&crate_config, true)?;
|
cfg::Platform::Web => {
|
||||||
|
// generate dev-index page
|
||||||
|
Serve::regen_dev_page(&crate_config)?;
|
||||||
|
|
||||||
match &crate_config.executable {
|
// start the develop server
|
||||||
crate::ExecutableType::Binary(name)
|
server::web::startup(self.serve.port, crate_config.clone(), self.serve.open)
|
||||||
| crate::ExecutableType::Lib(name)
|
.await?;
|
||||||
| crate::ExecutableType::Example(name) => {
|
}
|
||||||
let mut file = crate_config.out_dir.join(name);
|
cfg::Platform::Desktop => {
|
||||||
if cfg!(windows) {
|
server::desktop::startup(crate_config.clone()).await?;
|
||||||
file.set_extension("exe");
|
|
||||||
}
|
|
||||||
Command::new(file.to_str().unwrap())
|
|
||||||
.stdout(Stdio::inherit())
|
|
||||||
.output()?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::error::Result;
|
use crate::{cfg::Platform, error::Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@@ -73,7 +73,7 @@ impl Default for DioxusConfig {
|
|||||||
Self {
|
Self {
|
||||||
application: ApplicationConfig {
|
application: ApplicationConfig {
|
||||||
name: "dioxus".into(),
|
name: "dioxus".into(),
|
||||||
default_platform: "web".to_string(),
|
default_platform: Platform::Web,
|
||||||
out_dir: Some(PathBuf::from("dist")),
|
out_dir: Some(PathBuf::from("dist")),
|
||||||
asset_dir: Some(PathBuf::from("public")),
|
asset_dir: Some(PathBuf::from("public")),
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ impl Default for DioxusConfig {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ApplicationConfig {
|
pub struct ApplicationConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub default_platform: String,
|
pub default_platform: Platform,
|
||||||
pub out_dir: Option<PathBuf>,
|
pub out_dir: Option<PathBuf>,
|
||||||
pub asset_dir: Option<PathBuf>,
|
pub asset_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -1,449 +1,27 @@
|
|||||||
use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result};
|
use crate::{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 cargo_metadata::diagnostic::Diagnostic;
|
use cargo_metadata::diagnostic::Diagnostic;
|
||||||
use dioxus_core::Template;
|
use dioxus_core::Template;
|
||||||
use dioxus_html::HtmlCtx;
|
use dioxus_html::HtmlCtx;
|
||||||
use dioxus_rsx::hot_reload::*;
|
use dioxus_rsx::hot_reload::*;
|
||||||
use notify::{RecommendedWatcher, Watcher};
|
use notify::{RecommendedWatcher, Watcher};
|
||||||
use std::{
|
use std::{
|
||||||
net::UdpSocket,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::Command,
|
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
use tokio::sync::broadcast::{self, Sender};
|
use tokio::sync::broadcast::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::*;
|
|
||||||
|
|
||||||
mod output;
|
mod output;
|
||||||
use output::*;
|
use output::*;
|
||||||
|
pub mod desktop;
|
||||||
pub struct BuildManager {
|
pub mod web;
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets up a file watcher
|
/// 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,
|
config: &CrateConfig,
|
||||||
port: u16,
|
web_info: Option<WebServerInfo>,
|
||||||
watcher_ip: String,
|
|
||||||
reload_tx: Sender<()>,
|
|
||||||
) -> Result<RecommendedWatcher> {
|
) -> Result<RecommendedWatcher> {
|
||||||
let build_manager = BuildManager {
|
|
||||||
config: config.clone(),
|
|
||||||
reload_tx,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut last_update_time = chrono::Local::now().timestamp();
|
let mut last_update_time = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
// file watcher: check file change
|
// file watcher: check file change
|
||||||
@@ -460,20 +38,19 @@ async fn setup_file_watcher(
|
|||||||
let config = watcher_config.clone();
|
let config = watcher_config.clone();
|
||||||
if let Ok(e) = info {
|
if let Ok(e) = info {
|
||||||
if chrono::Local::now().timestamp() > last_update_time {
|
if chrono::Local::now().timestamp() > last_update_time {
|
||||||
match build_manager.rebuild() {
|
match build_with() {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
last_update_time = chrono::Local::now().timestamp();
|
last_update_time = chrono::Local::now().timestamp();
|
||||||
|
|
||||||
#[allow(clippy::redundant_clone)]
|
#[allow(clippy::redundant_clone)]
|
||||||
print_console_info(
|
print_console_info(
|
||||||
&watcher_ip,
|
|
||||||
port,
|
|
||||||
&config,
|
&config,
|
||||||
PrettierOptions {
|
PrettierOptions {
|
||||||
changed: e.paths.clone(),
|
changed: e.paths.clone(),
|
||||||
warnings: res.warnings,
|
warnings: res.warnings,
|
||||||
elapsed_time: res.elapsed_time,
|
elapsed_time: res.elapsed_time,
|
||||||
},
|
},
|
||||||
|
web_info.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
@@ -502,13 +79,12 @@ async fn setup_file_watcher(
|
|||||||
|
|
||||||
// Todo: reduce duplication and merge with setup_file_watcher()
|
// Todo: reduce duplication and merge with setup_file_watcher()
|
||||||
/// Sets up a file watcher with hot reload
|
/// 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,
|
config: &CrateConfig,
|
||||||
port: u16,
|
|
||||||
watcher_ip: String,
|
|
||||||
hot_reload_tx: Sender<Template<'static>>,
|
hot_reload_tx: Sender<Template<'static>>,
|
||||||
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||||
build_manager: Arc<BuildManager>,
|
build_with: F,
|
||||||
|
web_info: Option<WebServerInfo>,
|
||||||
) -> Result<RecommendedWatcher> {
|
) -> Result<RecommendedWatcher> {
|
||||||
// file watcher: check file change
|
// file watcher: check file change
|
||||||
let allow_watch_path = config
|
let allow_watch_path = config
|
||||||
@@ -533,17 +109,16 @@ async fn setup_file_watcher_hot_reload(
|
|||||||
for path in evt.paths.clone() {
|
for path in evt.paths.clone() {
|
||||||
// if this is not a rust file, rebuild the whole project
|
// if this is not a rust file, rebuild the whole project
|
||||||
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
|
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
|
||||||
match build_manager.rebuild() {
|
match build_with() {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
print_console_info(
|
print_console_info(
|
||||||
&watcher_ip,
|
|
||||||
port,
|
|
||||||
&config,
|
&config,
|
||||||
PrettierOptions {
|
PrettierOptions {
|
||||||
changed: evt.paths,
|
changed: evt.paths,
|
||||||
warnings: res.warnings,
|
warnings: res.warnings,
|
||||||
elapsed_time: res.elapsed_time,
|
elapsed_time: res.elapsed_time,
|
||||||
},
|
},
|
||||||
|
web_info.clone(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -560,17 +135,16 @@ async fn setup_file_watcher_hot_reload(
|
|||||||
messages.extend(msgs);
|
messages.extend(msgs);
|
||||||
}
|
}
|
||||||
Ok(UpdateResult::NeedsRebuild) => {
|
Ok(UpdateResult::NeedsRebuild) => {
|
||||||
match build_manager.rebuild() {
|
match build_with() {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
print_console_info(
|
print_console_info(
|
||||||
&watcher_ip,
|
|
||||||
port,
|
|
||||||
&config,
|
&config,
|
||||||
PrettierOptions {
|
PrettierOptions {
|
||||||
changed: evt.paths,
|
changed: evt.paths,
|
||||||
warnings: res.warnings,
|
warnings: res.warnings,
|
||||||
elapsed_time: res.elapsed_time,
|
elapsed_time: res.elapsed_time,
|
||||||
},
|
},
|
||||||
|
web_info.clone(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -606,50 +180,3 @@ async fn setup_file_watcher_hot_reload(
|
|||||||
|
|
||||||
Ok(watcher)
|
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();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,17 @@ pub struct PrettierOptions {
|
|||||||
pub elapsed_time: u128,
|
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") {
|
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||||
"cls"
|
"cls"
|
||||||
} else {
|
} 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) {
|
if let Some(WebServerInfo { ip, port }) = web_info {
|
||||||
println!(
|
if config.dioxus_config.web.https.enabled == Some(true) {
|
||||||
"\t> Local : {}",
|
println!(
|
||||||
format!("https://localhost:{}/", port).blue()
|
"\t> Local : {}",
|
||||||
);
|
format!("https://localhost:{}/", port).blue()
|
||||||
println!(
|
);
|
||||||
"\t> Network : {}",
|
println!(
|
||||||
format!("https://{}:{}/", ip, port).blue()
|
"\t> Network : {}",
|
||||||
);
|
format!("https://{}:{}/", ip, port).blue()
|
||||||
println!("\t> HTTPS : {}", "Enabled".to_string().green());
|
);
|
||||||
} else {
|
println!("\t> HTTPS : {}", "Enabled".to_string().green());
|
||||||
println!(
|
} else {
|
||||||
"\t> Local : {}",
|
println!(
|
||||||
format!("http://localhost:{}/", port).blue()
|
"\t> Local : {}",
|
||||||
);
|
format!("http://localhost:{}/", port).blue()
|
||||||
println!(
|
);
|
||||||
"\t> Network : {}",
|
println!(
|
||||||
format!("http://{}:{}/", ip, port).blue()
|
"\t> Network : {}",
|
||||||
);
|
format!("http://{}:{}/", ip, port).blue()
|
||||||
println!("\t> HTTPS : {}", "Disabled".to_string().red());
|
);
|
||||||
|
println!("\t> HTTPS : {}", "Disabled".to_string().red());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
println!("\t> Profile : {}", profile.green());
|
println!("\t> Profile : {}", profile.green());
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ use dioxus_html::HtmlCtx;
|
|||||||
use dioxus_rsx::hot_reload::FileMap;
|
use dioxus_rsx::hot_reload::FileMap;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use super::BuildManager;
|
|
||||||
use crate::CrateConfig;
|
use crate::CrateConfig;
|
||||||
|
|
||||||
pub struct HotReloadState {
|
pub struct HotReloadState {
|
||||||
pub messages: broadcast::Sender<Template<'static>>,
|
pub messages: broadcast::Sender<Template<'static>>,
|
||||||
pub build_manager: Arc<BuildManager>,
|
|
||||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||||
pub watcher_config: CrateConfig,
|
pub watcher_config: CrateConfig,
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
File renamed without changes.
Reference in new issue
Block a user