mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
fix hotreloading issues in the CLI
This commit is contained in:
parent
d180f569cf
commit
ad7a350d2e
37 changed files with 818 additions and 805 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -3,8 +3,8 @@
|
|||
"[toml]": {
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
"rust-analyzer.check.workspace": false,
|
||||
// "rust-analyzer.check.workspace": true,
|
||||
"rust-analyzer.check.workspace": false,
|
||||
"rust-analyzer.check.features": "all",
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
"rust-analyzer.check.allTargets": true
|
||||
|
|
60
Cargo.toml
60
Cargo.toml
|
@ -48,7 +48,7 @@ members = [
|
|||
"packages/playwright-tests/web",
|
||||
"packages/playwright-tests/fullstack",
|
||||
]
|
||||
exclude = ["examples/mobile_demo", "examples/openid_connect_demo",]
|
||||
exclude = ["examples/mobile_demo", "examples/openid_connect_demo"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0-alpha.0"
|
||||
|
@ -60,21 +60,21 @@ dioxus-lib = { path = "packages/dioxus-lib", version = "0.5.0-alpha.0" }
|
|||
dioxus-core = { path = "packages/core", version = "0.5.0-alpha.0" }
|
||||
dioxus-core-macro = { path = "packages/core-macro", version = "0.5.0-alpha.0" }
|
||||
dioxus-config-macro = { path = "packages/config-macro", version = "0.5.0-alpha.0" }
|
||||
dioxus-router = { path = "packages/router", version = "0.5.0-alpha.0" }
|
||||
dioxus-router = { path = "packages/router", version = "0.5.0-alpha.0" }
|
||||
dioxus-router-macro = { path = "packages/router-macro", version = "0.5.0-alpha.0" }
|
||||
dioxus-html = { path = "packages/html", version = "0.5.0-alpha.0" }
|
||||
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.5.0-alpha.0" }
|
||||
dioxus-html = { path = "packages/html", version = "0.5.0-alpha.0" }
|
||||
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.5.0-alpha.0" }
|
||||
dioxus-hooks = { path = "packages/hooks", version = "0.5.0-alpha.0" }
|
||||
dioxus-web = { path = "packages/web", version = "0.5.0-alpha.0" }
|
||||
dioxus-ssr = { path = "packages/ssr", version = "0.5.0-alpha.0", default-features = false }
|
||||
dioxus-desktop = { path = "packages/desktop", version = "0.5.0-alpha.0" }
|
||||
dioxus-mobile = { path = "packages/mobile", version = "0.5.0-alpha.0" }
|
||||
dioxus-mobile = { path = "packages/mobile", version = "0.5.0-alpha.0" }
|
||||
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.5.0-alpha.0" }
|
||||
dioxus-liveview = { path = "packages/liveview", version = "0.5.0-alpha.0" }
|
||||
dioxus-autofmt = { path = "packages/autofmt", version = "0.5.0-alpha.0" }
|
||||
dioxus-check = { path = "packages/check", version = "0.5.0-alpha.0" }
|
||||
dioxus-rsx = { path = "packages/rsx", version = "0.5.0-alpha.0" }
|
||||
dioxus-tui = { path = "packages/dioxus-tui", version = "0.5.0-alpha.0" }
|
||||
dioxus-liveview = { path = "packages/liveview", version = "0.5.0-alpha.0" }
|
||||
dioxus-autofmt = { path = "packages/autofmt", version = "0.5.0-alpha.0" }
|
||||
dioxus-check = { path = "packages/check", version = "0.5.0-alpha.0" }
|
||||
dioxus-rsx = { path = "packages/rsx", version = "0.5.0-alpha.0" }
|
||||
dioxus-tui = { path = "packages/dioxus-tui", version = "0.5.0-alpha.0" }
|
||||
plasmo = { path = "packages/plasmo", version = "0.5.0-alpha.0" }
|
||||
dioxus-native-core = { path = "packages/native-core", version = "0.5.0-alpha.0" }
|
||||
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.5.0-alpha.0" }
|
||||
|
@ -84,7 +84,7 @@ dioxus-cli-config = { path = "packages/cli-config", version = "0.5.0-alpha.0" }
|
|||
generational-box = { path = "packages/generational-box", version = "0.5.0-alpha.0" }
|
||||
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.5.0-alpha.0" }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.5.0-alpha.0" }
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0-alpha.0", default-features = false}
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0-alpha.0", default-features = false }
|
||||
dioxus-ext = { path = "packages/extension", version = "0.4.0" }
|
||||
tracing = "0.1.37"
|
||||
tracing-futures = "0.2.5"
|
||||
|
@ -100,16 +100,16 @@ thiserror = "1.0.40"
|
|||
prettyplease = { package = "prettier-please", version = "0.2", features = [
|
||||
"verbatim",
|
||||
] }
|
||||
manganis-cli-support = { version = "0.2.0", features = [
|
||||
"webp",
|
||||
"html",
|
||||
] }
|
||||
manganis-cli-support = { version = "0.2.0", features = ["webp", "html"] }
|
||||
manganis = { version = "0.2.0" }
|
||||
|
||||
interprocess = { version = "1.2.1" }
|
||||
# interprocess = { git = "https://github.com/kotauskas/interprocess" }
|
||||
|
||||
lru = "0.12.2"
|
||||
async-trait = "0.1.77"
|
||||
axum = "0.7.0"
|
||||
axum-server = {version = "0.6.0", default-features = false}
|
||||
axum-server = { version = "0.6.0", default-features = false }
|
||||
tower = "0.4.13"
|
||||
http = "1.0.0"
|
||||
tower-http = "0.5.1"
|
||||
|
@ -117,10 +117,24 @@ hyper = "1.0.0"
|
|||
hyper-rustls = "0.26.0"
|
||||
serde_json = "1.0.61"
|
||||
serde = "1.0.61"
|
||||
syn = "2.0"
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
axum_session = "0.12.1"
|
||||
axum_session_auth = "0.12.1"
|
||||
axum-extra = "0.9.2"
|
||||
reqwest = "0.11.24"
|
||||
owo-colors = "4.0.0"
|
||||
|
||||
# Enable a small amount of optimization in debug mode
|
||||
[profile.cli-dev]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
|
||||
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
|
||||
[profile.cli-dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
|
||||
# This is a "virtual package"
|
||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||
|
@ -139,9 +153,9 @@ rust-version = "1.60.0"
|
|||
publish = false
|
||||
|
||||
[dependencies]
|
||||
manganis = { workspace = true, optional = true}
|
||||
reqwest = { version = "0.11.9", features = ["json"], optional = true}
|
||||
http-range = {version = "0.1.5", optional = true }
|
||||
manganis = { workspace = true, optional = true }
|
||||
reqwest = { version = "0.11.9", features = ["json"], optional = true }
|
||||
http-range = { version = "0.1.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true, features = ["router"] }
|
||||
|
@ -155,7 +169,13 @@ form_urlencoded = "1.2.0"
|
|||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
getrandom = { version = "0.2.12", features = ["js"] }
|
||||
tokio = { version = "1.16.1", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
|
||||
tokio = { version = "1.16.1", default-features = false, features = [
|
||||
"sync",
|
||||
"macros",
|
||||
"io-util",
|
||||
"rt",
|
||||
"time",
|
||||
] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "1.16.1", features = ["full"] }
|
||||
|
|
|
@ -13,8 +13,8 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
[dependencies]
|
||||
dioxus-rsx = { workspace = true }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] }
|
||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||
quote = {workspace = true }
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
|
||||
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "2.0.3"
|
||||
|
|
|
@ -77,8 +77,8 @@ fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
|
|||
fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
|
||||
if let Pat::Ident(ident) = &local.pat {
|
||||
if is_hook_ident(&ident.ident) {
|
||||
if let Some((_, expr)) = &local.init {
|
||||
if let syn::Expr::Closure(closure) = &**expr {
|
||||
if let Some(init) = &local.init {
|
||||
if let syn::Expr::Closure(closure) = init.expr.as_ref() {
|
||||
return Some(&closure.body);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,8 +301,10 @@ pub struct WebProxyConfig {
|
|||
pub struct WebWatcherConfig {
|
||||
#[serde(default = "watch_path_default")]
|
||||
pub watch_path: Vec<PathBuf>,
|
||||
|
||||
#[serde(default)]
|
||||
pub reload_html: bool,
|
||||
|
||||
#[serde(default = "true_bool")]
|
||||
pub index_on_404: bool,
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ log = "0.4.14"
|
|||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
toml = {workspace = true}
|
||||
toml = { workspace = true }
|
||||
fs_extra = "1.2.0"
|
||||
cargo_toml = "0.18.0"
|
||||
futures-util = { workspace = true }
|
||||
|
@ -78,7 +78,7 @@ toml_edit = "0.21.0"
|
|||
tauri-bundler = { version = "=1.4.*", features = ["native-tls-vendored"] }
|
||||
|
||||
# formatting
|
||||
syn = { version = "2.0" }
|
||||
syn = { workspace = true }
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
manganis-cli-support = { workspace = true, features = ["webp", "html"] }
|
||||
|
@ -90,7 +90,8 @@ 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" }
|
||||
interprocess = { workspace = true }
|
||||
# interprocess-docfix = { version = "1.2.2" }
|
||||
ignore = "0.4.22"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
//! Construct version in the `commit-hash date channel` format
|
||||
|
||||
use std::{env, path::PathBuf, process::Command};
|
||||
|
||||
fn main() {
|
||||
set_rerun();
|
||||
set_commit_info();
|
||||
}
|
||||
|
||||
fn set_rerun() {
|
||||
let mut manifest_dir = PathBuf::from(
|
||||
env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is always set by cargo."),
|
||||
);
|
||||
|
||||
while manifest_dir.parent().is_some() {
|
||||
let head_ref = manifest_dir.join(".git/HEAD");
|
||||
if head_ref.exists() {
|
||||
println!("cargo:rerun-if-changed={}", head_ref.display());
|
||||
return;
|
||||
}
|
||||
|
||||
manifest_dir.pop();
|
||||
}
|
||||
|
||||
println!("cargo:warning=Could not find `.git/HEAD` from manifest dir!");
|
||||
}
|
||||
|
||||
fn set_commit_info() {
|
||||
let output = match Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
.arg("--date=short")
|
||||
.arg("--format=%H %h %cd")
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => output,
|
||||
_ => return,
|
||||
};
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
let mut parts = stdout.split_whitespace();
|
||||
let mut next = || parts.next().unwrap();
|
||||
println!("cargo:rustc-env=RA_COMMIT_HASH={}", next());
|
||||
println!("cargo:rustc-env=RA_COMMIT_SHORT_HASH={}", next());
|
||||
println!("cargo:rustc-env=RA_COMMIT_DATE={}", next())
|
||||
}
|
1
packages/cli/rustfmt.toml
Normal file
1
packages/cli/rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
imports_granularity = "Crate"
|
|
@ -3,10 +3,9 @@ use crate::{
|
|||
error::{Error, Result},
|
||||
tools::Tool,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use cargo_metadata::{diagnostic::Diagnostic, Message};
|
||||
use dioxus_cli_config::crate_root;
|
||||
use dioxus_cli_config::CrateConfig;
|
||||
use dioxus_cli_config::ExecutableType;
|
||||
use dioxus_cli_config::{crate_root, CrateConfig, ExecutableType};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use manganis_cli_support::{AssetManifest, ManganisSupportGuard};
|
||||
|
@ -67,7 +66,6 @@ impl ExecWithRustFlagsSetter for subprocess::Exec {
|
|||
/// Note: `rust_flags` argument is only used for the fullstack platform.
|
||||
pub fn build(
|
||||
config: &CrateConfig,
|
||||
_: bool,
|
||||
skip_assets: bool,
|
||||
rust_flags: Option<String>,
|
||||
) -> Result<BuildResult> {
|
||||
|
@ -103,6 +101,7 @@ pub fn build(
|
|||
let wasm_check_command = std::process::Command::new("rustup")
|
||||
.args(["show"])
|
||||
.output()?;
|
||||
|
||||
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
|
||||
if !wasm_check_output.contains("wasm32-unknown-unknown") {
|
||||
log::info!("wasm32-unknown-unknown target not detected, installing..");
|
||||
|
@ -163,9 +162,10 @@ pub fn build(
|
|||
let input_path = warning_messages
|
||||
.output_location
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.context("No output location found")?
|
||||
.with_extension("wasm");
|
||||
|
||||
log::info!("Running wasm-bindgen");
|
||||
let bindgen_result = panic::catch_unwind(move || {
|
||||
// [3] Bindgen the final binary for use easy linking
|
||||
let mut bindgen_builder = Bindgen::new();
|
||||
|
@ -183,11 +183,13 @@ pub fn build(
|
|||
.generate(&bindgen_outdir)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
if bindgen_result.is_err() {
|
||||
return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
|
||||
}
|
||||
|
||||
// check binaryen:wasm-opt tool
|
||||
log::info!("Running optimization with wasm-opt...");
|
||||
let dioxus_tools = dioxus_config.application.tools.clone();
|
||||
if dioxus_tools.contains_key("binaryen") {
|
||||
let info = dioxus_tools.get("binaryen").unwrap();
|
||||
|
@ -221,6 +223,8 @@ pub fn build(
|
|||
"Binaryen tool not found, you can use `dx tool add binaryen` to install it."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::info!("Skipping optimization with wasm-opt, binaryen tool not found.");
|
||||
}
|
||||
|
||||
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
|
||||
|
@ -271,6 +275,8 @@ pub fn build(
|
|||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
log::info!("Copying public assets to the output directory...");
|
||||
if asset_dir.is_dir() {
|
||||
for entry in std::fs::read_dir(config.asset_dir())?.flatten() {
|
||||
let path = entry.path();
|
||||
|
@ -294,6 +300,7 @@ pub fn build(
|
|||
}
|
||||
}
|
||||
|
||||
log::info!("Processing assets");
|
||||
let assets = if !skip_assets {
|
||||
let assets = asset_manifest(executable.executable(), config);
|
||||
process_assets(config, &assets)?;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::assets::AssetConfigDropGuard;
|
||||
use crate::server::fullstack;
|
||||
use crate::{assets::AssetConfigDropGuard, server::fullstack};
|
||||
use dioxus_cli_config::Platform;
|
||||
|
||||
use super::*;
|
||||
|
@ -58,7 +57,7 @@ impl Build {
|
|||
let build_result = match platform {
|
||||
Platform::Web => {
|
||||
// `rust_flags` are used by fullstack's client build.
|
||||
crate::builder::build(&crate_config, false, self.build.skip_assets, rust_flags)?
|
||||
crate::builder::build(&crate_config, self.build.skip_assets, rust_flags)?
|
||||
}
|
||||
Platform::Desktop => {
|
||||
// Since desktop platform doesn't use `rust_flags`, this
|
||||
|
@ -83,7 +82,6 @@ impl Build {
|
|||
};
|
||||
crate::builder::build(
|
||||
&web_config,
|
||||
false,
|
||||
self.build.skip_assets,
|
||||
Some(client_rust_flags),
|
||||
)?;
|
||||
|
|
|
@ -85,8 +85,8 @@ pub struct ConfigOptsServe {
|
|||
#[clap(default_value_t = 8080)]
|
||||
pub port: u16,
|
||||
|
||||
/// Open the app in the default browser [default: false]
|
||||
#[clap(long)]
|
||||
/// Open the app in the default browser [default: true]
|
||||
#[clap(long, default_value_t = true)]
|
||||
#[serde(default)]
|
||||
pub open: bool,
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::*;
|
|||
use cargo_generate::{GenerateArgs, TemplatePath};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
#[clap(name = "create")]
|
||||
#[clap(name = "new")]
|
||||
pub struct Create {
|
||||
/// Template path
|
||||
#[clap(default_value = "gh:dioxuslabs/dioxus-template", long)]
|
||||
|
|
|
@ -10,7 +10,6 @@ pub mod init;
|
|||
pub mod plugin;
|
||||
pub mod serve;
|
||||
pub mod translate;
|
||||
pub mod version;
|
||||
|
||||
use crate::{
|
||||
cfg::{ConfigOptsBuild, ConfigOptsServe},
|
||||
|
@ -57,10 +56,11 @@ pub enum Commands {
|
|||
/// Build, watch & serve the Rust WASM app and all of its assets.
|
||||
Serve(serve::Serve),
|
||||
|
||||
/// Create a new project for Dioxus.
|
||||
Create(create::Create),
|
||||
/// Create a new project for Dioxus.a
|
||||
New(create::Create),
|
||||
|
||||
/// Init a new project for Dioxus
|
||||
/// Init a new project for Dioxus in an existing directory.
|
||||
/// Will attempt to keep your project in a good state
|
||||
Init(init::Init),
|
||||
|
||||
/// Clean output artifacts.
|
||||
|
@ -69,10 +69,6 @@ pub enum Commands {
|
|||
/// Bundle the Rust desktop app and all of its assets.
|
||||
Bundle(bundle::Bundle),
|
||||
|
||||
/// Print the version of this extension
|
||||
#[clap(name = "version")]
|
||||
Version(version::Version),
|
||||
|
||||
/// Format some rsx
|
||||
#[clap(name = "fmt")]
|
||||
Autoformat(autoformat::Autoformat),
|
||||
|
@ -97,11 +93,10 @@ impl Display for Commands {
|
|||
Commands::Build(_) => write!(f, "build"),
|
||||
Commands::Translate(_) => write!(f, "translate"),
|
||||
Commands::Serve(_) => write!(f, "serve"),
|
||||
Commands::Create(_) => write!(f, "create"),
|
||||
Commands::New(_) => write!(f, "create"),
|
||||
Commands::Init(_) => write!(f, "init"),
|
||||
Commands::Clean(_) => write!(f, "clean"),
|
||||
Commands::Config(_) => write!(f, "config"),
|
||||
Commands::Version(_) => write!(f, "version"),
|
||||
Commands::Autoformat(_) => write!(f, "fmt"),
|
||||
Commands::Check(_) => write!(f, "check"),
|
||||
Commands::Bundle(_) => write!(f, "bundle"),
|
||||
|
|
|
@ -61,24 +61,14 @@ impl Serve {
|
|||
|
||||
let platform = platform.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||
|
||||
// start the develop server
|
||||
use server::{desktop, fullstack, web};
|
||||
match platform {
|
||||
Platform::Web => {
|
||||
// start the develop server
|
||||
server::web::startup(
|
||||
self.serve.port,
|
||||
crate_config.clone(),
|
||||
self.serve.open,
|
||||
self.serve.skip_assets,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Platform::Desktop => {
|
||||
server::desktop::startup(crate_config.clone(), &serve_cfg).await?;
|
||||
}
|
||||
Platform::Fullstack => {
|
||||
server::fullstack::startup(crate_config.clone(), &serve_cfg).await?;
|
||||
}
|
||||
Platform::Web => web::startup(crate_config.clone(), &serve_cfg).await?,
|
||||
Platform::Desktop => desktop::startup(crate_config.clone(), &serve_cfg).await?,
|
||||
Platform::Fullstack => fullstack::startup(crate_config.clone(), &serve_cfg).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
/// Print the version of this extension
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "version")]
|
||||
pub struct Version {}
|
||||
|
||||
impl Version {
|
||||
pub fn version(self) -> VersionInfo {
|
||||
version()
|
||||
}
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Information about the git repository where rust-analyzer was built from.
|
||||
pub struct CommitInfo {
|
||||
pub short_commit_hash: &'static str,
|
||||
pub commit_hash: &'static str,
|
||||
pub commit_date: &'static str,
|
||||
}
|
||||
|
||||
/// Cargo's version.
|
||||
pub struct VersionInfo {
|
||||
/// rust-analyzer's version, such as "1.57.0", "1.58.0-beta.1", "1.59.0-nightly", etc.
|
||||
pub version: &'static str,
|
||||
|
||||
/// The release channel we were built for (stable/beta/nightly/dev).
|
||||
///
|
||||
/// `None` if not built via rustbuild.
|
||||
pub release_channel: Option<&'static str>,
|
||||
|
||||
/// Information about the Git repository we may have been built from.
|
||||
///
|
||||
/// `None` if not built from a git repo.
|
||||
pub commit_info: Option<CommitInfo>,
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.version)?;
|
||||
|
||||
if let Some(ci) = &self.commit_info {
|
||||
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about cargo's version.
|
||||
pub const fn version() -> VersionInfo {
|
||||
let version = match option_env!("CARGO_PKG_VERSION") {
|
||||
Some(x) => x,
|
||||
None => "0.0.0",
|
||||
};
|
||||
|
||||
let release_channel = option_env!("CFG_RELEASE_CHANNEL");
|
||||
let commit_info = match (
|
||||
option_env!("RA_COMMIT_SHORT_HASH"),
|
||||
option_env!("RA_COMMIT_HASH"),
|
||||
option_env!("RA_COMMIT_DATE"),
|
||||
) {
|
||||
(Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo {
|
||||
short_commit_hash,
|
||||
commit_hash,
|
||||
commit_date,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
VersionInfo {
|
||||
version,
|
||||
release_channel,
|
||||
commit_info,
|
||||
}
|
||||
}
|
|
@ -7,35 +7,6 @@ use dioxus_cli::*;
|
|||
|
||||
use Commands::*;
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.exec()
|
||||
.map_err(Error::CargoMetadata)?;
|
||||
let package = if let Some(bin) = bin {
|
||||
metadata
|
||||
.workspace_packages()
|
||||
.into_iter()
|
||||
.find(|p| p.name == bin)
|
||||
.ok_or(Error::CargoError(format!("no such package: {}", bin)))?
|
||||
} else {
|
||||
metadata
|
||||
.root_package()
|
||||
.ok_or(Error::CargoError("no root package?".to_string()))?
|
||||
};
|
||||
|
||||
let crate_dir = package
|
||||
.manifest_path
|
||||
.parent()
|
||||
.ok_or(Error::CargoError("couldn't take parent dir".to_string()))?;
|
||||
|
||||
Ok(crate_dir.into())
|
||||
}
|
||||
|
||||
/// Simplifies error messages that use the same pattern.
|
||||
fn error_wrapper(message: &str) -> String {
|
||||
format!("🚫 {message}:")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
@ -47,7 +18,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
.translate()
|
||||
.context(error_wrapper("Translation of HTML into RSX failed")),
|
||||
|
||||
Create(opts) => opts
|
||||
New(opts) => opts
|
||||
.create()
|
||||
.context(error_wrapper("Creating new project failed")),
|
||||
|
||||
|
@ -74,12 +45,6 @@ async fn main() -> anyhow::Result<()> {
|
|||
.await
|
||||
.context(error_wrapper("Error checking RSX")),
|
||||
|
||||
Version(opt) => {
|
||||
let version = opt.version();
|
||||
println!("{}", version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
action => {
|
||||
let bin = get_bin(args.bin)?;
|
||||
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||
|
@ -119,3 +84,32 @@ async fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.exec()
|
||||
.map_err(Error::CargoMetadata)?;
|
||||
let package = if let Some(bin) = bin {
|
||||
metadata
|
||||
.workspace_packages()
|
||||
.into_iter()
|
||||
.find(|p| p.name == bin)
|
||||
.ok_or(Error::CargoError(format!("no such package: {}", bin)))?
|
||||
} else {
|
||||
metadata
|
||||
.root_package()
|
||||
.ok_or(Error::CargoError("no root package?".to_string()))?
|
||||
};
|
||||
|
||||
let crate_dir = package
|
||||
.manifest_path
|
||||
.parent()
|
||||
.ok_or(Error::CargoError("couldn't take parent dir".to_string()))?;
|
||||
|
||||
Ok(crate_dir.into())
|
||||
}
|
||||
|
||||
/// Simplifies error messages that use the same pattern.
|
||||
fn error_wrapper(message: &str) -> String {
|
||||
format!("🚫 {message}:")
|
||||
}
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
use crate::server::Platform;
|
||||
use crate::{
|
||||
cfg::ConfigOptsServe,
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions},
|
||||
setup_file_watcher,
|
||||
setup_file_watcher, Platform,
|
||||
},
|
||||
BuildResult, Result,
|
||||
};
|
||||
use dioxus_cli_config::CrateConfig;
|
||||
|
||||
use dioxus_cli_config::{CrateConfig, ExecutableType};
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use interprocess_docfix::local_socket::LocalSocketListener;
|
||||
use std::fs::create_dir_all;
|
||||
use interprocess::local_socket::LocalSocketListener;
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
process::{Child, Command},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
|
@ -33,13 +31,7 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
|
|||
config: CrateConfig,
|
||||
serve_cfg: &ConfigOptsServe,
|
||||
) -> 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);
|
||||
});
|
||||
set_ctrl_c(&config);
|
||||
|
||||
let hot_reload_state = match config.hot_reload {
|
||||
true => {
|
||||
|
@ -67,6 +59,16 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn set_ctrl_c(config: &CrateConfig) {
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
async fn serve<P: Platform + Send + 'static>(
|
||||
config: CrateConfig,
|
||||
|
@ -169,12 +171,13 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||
|
||||
let mut hot_reload_rx = hot_reload_state.messages.subscribe();
|
||||
|
||||
while let Ok(template) = hot_reload_rx.recv().await {
|
||||
while let Ok(msg) = 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) {
|
||||
if send_msg(msg.clone(), channel) {
|
||||
i += 1;
|
||||
} else {
|
||||
channels.remove(i);
|
||||
|
|
|
@ -3,10 +3,14 @@ use dioxus_cli_config::CrateConfig;
|
|||
|
||||
use cargo_metadata::diagnostic::Diagnostic;
|
||||
use dioxus_core::Template;
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast::{self};
|
||||
|
||||
mod output;
|
||||
|
@ -15,7 +19,15 @@ pub mod desktop;
|
|||
pub mod fullstack;
|
||||
pub mod web;
|
||||
|
||||
/// Sets up a file watcher
|
||||
#[derive(Clone)]
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<HotReloadMsg>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
}
|
||||
|
||||
/// Sets up a file watcher.
|
||||
///
|
||||
/// Will attempt to hotreload HTML, RSX (.rs), and CSS
|
||||
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
build_with: F,
|
||||
config: &CrateConfig,
|
||||
|
@ -25,124 +37,184 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
|||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
// file watcher: check file change
|
||||
let allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
|
||||
let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
|
||||
|
||||
let watcher_config = config.clone();
|
||||
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
|
||||
let config = watcher_config.clone();
|
||||
if let Ok(e) = info {
|
||||
match e.kind {
|
||||
notify::EventKind::Create(_)
|
||||
| notify::EventKind::Remove(_)
|
||||
| notify::EventKind::Modify(_) => {
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
let mut needs_full_rebuild;
|
||||
if let Some(hot_reload) = &hot_reload {
|
||||
// find changes to the rsx in the file
|
||||
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
|
||||
let mut messages: Vec<Template> = Vec::new();
|
||||
// Extend the watch path to include the assets directory
|
||||
allow_watch_path.push(config.dioxus_config.application.asset_dir.clone());
|
||||
|
||||
// In hot reload mode, we only need to rebuild if non-rsx code is changed
|
||||
needs_full_rebuild = false;
|
||||
// Create the file watcher
|
||||
let mut watcher = notify::recommended_watcher({
|
||||
let watcher_config = config.clone();
|
||||
move |info: notify::Result<notify::Event>| {
|
||||
let Ok(e) = info else {
|
||||
return;
|
||||
};
|
||||
|
||||
for path in &e.paths {
|
||||
// if this is not a rust file, rebuild the whole project
|
||||
let path_extension = path.extension().and_then(|p| p.to_str());
|
||||
if path_extension != Some("rs") {
|
||||
needs_full_rebuild = true;
|
||||
// if backup file generated will impact normal hot-reload, so ignore it
|
||||
if path_extension == Some("rs~") {
|
||||
needs_full_rebuild = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Workaround for notify and vscode-like editor:
|
||||
// when edit & save a file in vscode, there will be two notifications,
|
||||
// the first one is a file with empty content.
|
||||
// filter the empty file notification to avoid false rebuild during hot-reload
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match rsx_file_map.update_rsx(path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
needs_full_rebuild = false;
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
needs_full_rebuild = true;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needs_full_rebuild {
|
||||
// Reset the file map to the new state of the project
|
||||
let FileMapBuildResult {
|
||||
map: new_file_map,
|
||||
errors,
|
||||
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
*rsx_file_map = new_file_map;
|
||||
} else {
|
||||
for msg in messages {
|
||||
let _ = hot_reload.messages.send(msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
needs_full_rebuild = true;
|
||||
}
|
||||
|
||||
if needs_full_rebuild {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
#[allow(clippy::redundant_clone)]
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: e.paths.clone(),
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
watch_event(
|
||||
e,
|
||||
&mut last_update_time,
|
||||
&hot_reload,
|
||||
&watcher_config,
|
||||
&build_with,
|
||||
&web_info,
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
.expect("Failed to create file watcher - please ensure you have the required permissions to watch the specified directories.");
|
||||
|
||||
// Watch the specified paths
|
||||
for sub_path in allow_watch_path {
|
||||
if let Err(err) = watcher.watch(
|
||||
&config.crate_dir.join(sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
) {
|
||||
let path = &config.crate_dir.join(sub_path);
|
||||
let mode = notify::RecursiveMode::Recursive;
|
||||
|
||||
if let Err(err) = watcher.watch(path, mode) {
|
||||
log::warn!("Failed to watch path: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
fn watch_event<F>(
|
||||
event: notify::Event,
|
||||
last_update_time: &mut i64,
|
||||
hot_reload: &Option<HotReloadState>,
|
||||
config: &CrateConfig,
|
||||
build_with: &F,
|
||||
web_info: &Option<WebServerInfo>,
|
||||
) where
|
||||
F: Fn() -> Result<BuildResult> + Send + 'static,
|
||||
{
|
||||
// Ensure that we're tracking only modifications
|
||||
if !matches!(
|
||||
event.kind,
|
||||
notify::EventKind::Create(_) | notify::EventKind::Remove(_) | notify::EventKind::Modify(_)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that we're not rebuilding too frequently
|
||||
if chrono::Local::now().timestamp() <= *last_update_time {
|
||||
return;
|
||||
}
|
||||
|
||||
// By default we want to opt into a full rebuild, but hotreloading will actually set this force us
|
||||
let mut needs_full_rebuild = true;
|
||||
|
||||
if let Some(hot_reload) = &hot_reload {
|
||||
hotreload_files(hot_reload, &mut needs_full_rebuild, &event, &config);
|
||||
}
|
||||
|
||||
if needs_full_rebuild {
|
||||
full_rebuild(build_with, last_update_time, config, event, web_info);
|
||||
}
|
||||
}
|
||||
|
||||
fn full_rebuild<F>(
|
||||
build_with: &F,
|
||||
last_update_time: &mut i64,
|
||||
config: &CrateConfig,
|
||||
event: notify::Event,
|
||||
web_info: &Option<WebServerInfo>,
|
||||
) where
|
||||
F: Fn() -> Result<BuildResult> + Send + 'static,
|
||||
{
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
*last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
#[allow(clippy::redundant_clone)]
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: event.paths.clone(),
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
*last_update_time = chrono::Local::now().timestamp();
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hotreload_files(
|
||||
hot_reload: &HotReloadState,
|
||||
needs_full_rebuild: &mut bool,
|
||||
event: ¬ify::Event,
|
||||
config: &CrateConfig,
|
||||
) {
|
||||
// find changes to the rsx in the file
|
||||
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
|
||||
let mut messages: Vec<HotReloadMsg> = Vec::new();
|
||||
|
||||
// In hot reload mode, we only need to rebuild if non-rsx code is changed
|
||||
*needs_full_rebuild = false;
|
||||
|
||||
for path in &event.paths {
|
||||
// for various assets that might be linked in, we just try to hotreloading them forcefully
|
||||
// That is, unless they appear in an include! macro, in which case we need to a full rebuild....
|
||||
|
||||
// if this is not a rust file, rebuild the whole project
|
||||
let path_extension = path.extension().and_then(|p| p.to_str());
|
||||
|
||||
if path_extension != Some("rs") {
|
||||
*needs_full_rebuild = true;
|
||||
if path_extension == Some("rs~") {
|
||||
*needs_full_rebuild = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Workaround for notify and vscode-like editor:
|
||||
// when edit & save a file in vscode, there will be two notifications,
|
||||
// the first one is a file with empty content.
|
||||
// filter the empty file notification to avoid false rebuild during hot-reload
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match rsx_file_map.update_rsx(path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(
|
||||
msgs.into_iter()
|
||||
.map(|msg| HotReloadMsg::UpdateTemplate(msg)),
|
||||
);
|
||||
*needs_full_rebuild = false;
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
*needs_full_rebuild = true;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *needs_full_rebuild {
|
||||
// Reset the file map to the new state of the project
|
||||
let FileMapBuildResult {
|
||||
map: new_file_map,
|
||||
errors,
|
||||
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
*rsx_file_map = new_file_map;
|
||||
} else {
|
||||
for msg in messages {
|
||||
let _ = hot_reload.messages.send(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Platform {
|
||||
fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result<Self>
|
||||
where
|
||||
|
@ -150,8 +222,15 @@ pub(crate) trait Platform {
|
|||
fn rebuild(&mut self, config: &CrateConfig) -> Result<BuildResult>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<Template>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
}
|
||||
// Some("bin") => "application/octet-stream",
|
||||
// Some("css") => "text/css",
|
||||
// Some("csv") => "text/csv",
|
||||
// Some("html") => "text/html",
|
||||
// Some("ico") => "image/vnd.microsoft.icon",
|
||||
// Some("js") => "text/javascript",
|
||||
// Some("json") => "application/json",
|
||||
// Some("jsonld") => "application/ld+json",
|
||||
// Some("mjs") => "text/javascript",
|
||||
// Some("rtf") => "application/rtf",
|
||||
// Some("svg") => "image/svg+xml",
|
||||
// Some("mp4") => "video/mp4",
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use crate::server::Diagnostic;
|
||||
use colored::Colorize;
|
||||
use dioxus_cli_config::crate_root;
|
||||
use dioxus_cli_config::CrateConfig;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use dioxus_cli_config::{crate_root, CrateConfig};
|
||||
use std::{path::PathBuf, process::Command};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PrettierOptions {
|
||||
|
|
|
@ -1,54 +1,61 @@
|
|||
use crate::server::HotReloadState;
|
||||
use axum::{
|
||||
extract::{ws::Message, WebSocketUpgrade},
|
||||
extract::{
|
||||
ws::{Message, WebSocket},
|
||||
WebSocketUpgrade,
|
||||
},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
|
||||
pub async fn hot_reload_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Extension(state): Extension<HotReloadState>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
log::info!("🔥 Hot Reload WebSocket connected");
|
||||
{
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
{
|
||||
log::info!("🔮 Finding updates since last compile...");
|
||||
let templates: Vec<_> = {
|
||||
state
|
||||
.file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if socket
|
||||
.send(Message::Text(serde_json::to_string(&template).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("finished");
|
||||
}
|
||||
ws.on_upgrade(|socket| async move {
|
||||
let err = hotreload_loop(socket, state).await;
|
||||
|
||||
let mut rx = state.messages.subscribe();
|
||||
loop {
|
||||
if let Ok(rsx) = rx.recv().await {
|
||||
if socket
|
||||
.send(Message::Text(serde_json::to_string(&rsx).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
};
|
||||
}
|
||||
if let Err(err) = err {
|
||||
log::error!("Hotreload receiver failed: {}", err);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn hotreload_loop(mut socket: WebSocket, state: HotReloadState) -> anyhow::Result<()> {
|
||||
log::info!("🔥 Hot Reload WebSocket connected");
|
||||
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
log::info!("🔮 Finding updates since last compile...");
|
||||
|
||||
let templates = state
|
||||
.file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for template in templates {
|
||||
socket
|
||||
.send(Message::Text(serde_json::to_string(&template).unwrap()))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut rx = state.messages.subscribe();
|
||||
|
||||
loop {
|
||||
if let Ok(msg) = rx.recv().await {
|
||||
let msg = match msg {
|
||||
HotReloadMsg::UpdateTemplate(template) => {
|
||||
Message::Text(serde_json::to_string(&template).unwrap())
|
||||
}
|
||||
HotReloadMsg::UpdateAsset(_) => todo!(),
|
||||
HotReloadMsg::Shutdown => todo!(),
|
||||
};
|
||||
|
||||
socket.send(msg).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
builder,
|
||||
cfg::ConfigOptsServe,
|
||||
serve::Serve,
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions, WebServerInfo},
|
||||
|
@ -7,111 +8,51 @@ use crate::{
|
|||
},
|
||||
BuildResult, Result,
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{ws::Message, Extension, WebSocketUpgrade},
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use dioxus_cli_config::CrateConfig;
|
||||
use dioxus_cli_config::WebHttpsConfig;
|
||||
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use std::{
|
||||
net::UdpSocket,
|
||||
process::Command,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
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 crate::plugin::PluginManager;
|
||||
|
||||
mod proxy;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
mod hot_reload;
|
||||
use hot_reload::*;
|
||||
mod proxy;
|
||||
mod server;
|
||||
|
||||
struct WsReloadState {
|
||||
use server::*;
|
||||
|
||||
pub struct WsReloadState {
|
||||
update: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
pub async fn startup(
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
skip_assets: 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);
|
||||
});
|
||||
pub async fn startup(config: CrateConfig, serve_cfg: &ConfigOptsServe) -> Result<()> {
|
||||
set_ctrlc_handler(&config);
|
||||
|
||||
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
|
||||
|
||||
let hot_reload_state = match config.hot_reload {
|
||||
true => {
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
let mut hot_reload_state = None;
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
if config.hot_reload {
|
||||
hot_reload_state = Some(build_hotreload_filemap(&config));
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
Some(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
})
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
|
||||
serve(
|
||||
ip,
|
||||
port,
|
||||
config,
|
||||
start_browser,
|
||||
skip_assets,
|
||||
hot_reload_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
serve(ip, config, hot_reload_state, serve_cfg).await
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
skip_assets: bool,
|
||||
hot_reload_state: Option<HotReloadState>,
|
||||
opts: &ConfigOptsServe,
|
||||
) -> Result<()> {
|
||||
let skip_assets = opts.skip_assets;
|
||||
let port = opts.port;
|
||||
|
||||
// Since web platform doesn't use `rust_flags`, this argument is explicitly
|
||||
// set to `None`.
|
||||
let first_build_result = crate::builder::build(&config, false, skip_assets, None)?;
|
||||
let first_build_result = crate::builder::build(&config, skip_assets, None)?;
|
||||
|
||||
// generate dev-index page
|
||||
Serve::regen_dev_page(&config, first_build_result.assets.as_ref())?;
|
||||
|
@ -154,7 +95,7 @@ pub async fn serve(
|
|||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
Some(crate::server::output::WebServerInfo {
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
|
@ -164,230 +105,43 @@ pub async fn serve(
|
|||
let router = setup_router(config.clone(), ws_reload_state, hot_reload_state).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config, &config).await?;
|
||||
start_server(port, router, opts.open, rustls_config, &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) = if let Some(true) = web_config.mkcert {
|
||||
// mkcert, use it
|
||||
get_rustls_with_mkcert(web_config)?
|
||||
} else {
|
||||
// if mkcert not specified or false, don't use it
|
||||
get_rustls_without_mkcert(web_config)?
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
RustlsConfig::from_pem_file(cert_path, key_path).await?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
// 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()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((cert_path, key_path))
|
||||
}
|
||||
|
||||
fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
// get paths to cert & key
|
||||
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
|
||||
Ok((cert, key))
|
||||
} else {
|
||||
// missing cert or key
|
||||
Err("https is enabled but cert or key path is missing".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up and returns a router
|
||||
async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<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 mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Body::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
|
||||
.ok()
|
||||
.unwrap(),
|
||||
);
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.into_response()
|
||||
};
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
|
||||
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
.service(ServeDir::new(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 {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::convert::Infallible| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
|
||||
let base_path = format!("/{}", base_path.trim_matches('/'));
|
||||
Router::new()
|
||||
.route(&base_path, axum::routing::any_service(router))
|
||||
.fallback(get(move || {
|
||||
let base_path = base_path.clone();
|
||||
async move { format!("Outside of the base path: {}", base_path) }
|
||||
}))
|
||||
} else {
|
||||
router
|
||||
};
|
||||
|
||||
// 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,
|
||||
router: axum::Router,
|
||||
start_browser: bool,
|
||||
rustls: Option<RustlsConfig>,
|
||||
rustls: Option<axum_server::tls_rustls::RustlsConfig>,
|
||||
_config: &CrateConfig,
|
||||
) -> Result<()> {
|
||||
// If plugins, call on_serve_start event
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::on_serve_start(_config)?;
|
||||
crate::plugin::PluginManager::on_serve_start(_config)?;
|
||||
|
||||
// Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack)
|
||||
let addr = format!("[::]:{}", port).parse().unwrap();
|
||||
let addr: SocketAddr = 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)),
|
||||
Some(_) => _ = open::that(format!("https://localhost:{port}")),
|
||||
None => _ = open::that(format!("http://localhost:{port}")),
|
||||
}
|
||||
}
|
||||
|
||||
let svc = router.into_make_service();
|
||||
|
||||
// Start the server with or without rustls
|
||||
match rustls {
|
||||
Some(rustls) => {
|
||||
axum_server::bind_rustls(addr, rustls)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
Some(rustls) => axum_server::bind_rustls(addr, rustls).serve(svc).await?,
|
||||
None => {
|
||||
// Create a TCP listener bound to the address
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, router.into_make_service()).await?
|
||||
axum::serve(listener, svc).await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,43 +166,50 @@ fn get_ip() -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
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<()>, skip_assets: bool) -> Result<BuildResult> {
|
||||
fn build(
|
||||
config: &CrateConfig,
|
||||
reload_tx: &broadcast::Sender<()>,
|
||||
skip_assets: bool,
|
||||
) -> Result<BuildResult> {
|
||||
// Since web platform doesn't use `rust_flags`, this argument is explicitly
|
||||
// set to `None`.
|
||||
let result = builder::build(config, true, skip_assets, None)?;
|
||||
let result = std::panic::catch_unwind(|| builder::build(config, skip_assets, None))
|
||||
.map_err(|e| anyhow::anyhow!("Build failed: {e:?}"))?;
|
||||
|
||||
// change the websocket reload state to true;
|
||||
// the page will auto-reload.
|
||||
if config.dioxus_config.web.watcher.reload_html {
|
||||
let _ = Serve::regen_dev_page(config, result.assets.as_ref());
|
||||
if let Ok(assets) = result.as_ref().map(|x| x.assets.as_ref()) {
|
||||
let _ = Serve::regen_dev_page(config, assets);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = reload_tx.send(());
|
||||
Ok(result)
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn set_ctrlc_handler(config: &CrateConfig) {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = crate::plugin::PluginManager::on_serve_shutdown(&_crate_config);
|
||||
|
||||
std::process::exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
fn build_hotreload_filemap(config: &CrateConfig) -> HotReloadState {
|
||||
let FileMapBuildResult { map, errors } = FileMap::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
HotReloadState {
|
||||
messages: broadcast::channel(100).0.clone(),
|
||||
file_map: Arc::new(Mutex::new(map)).clone(),
|
||||
}
|
||||
}
|
||||
|
|
254
packages/cli/src/server/web/server.rs
Normal file
254
packages/cli/src/server/web/server.rs
Normal file
|
@ -0,0 +1,254 @@
|
|||
use std::{fs, io, process::Command, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
builder,
|
||||
serve::Serve,
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions, WebServerInfo},
|
||||
setup_file_watcher, HotReloadState,
|
||||
},
|
||||
BuildResult, Result,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{
|
||||
ws::{Message, WebSocket},
|
||||
Extension, WebSocketUpgrade,
|
||||
},
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use dioxus_cli_config::{CrateConfig, WebHttpsConfig};
|
||||
|
||||
use super::{hot_reload::*, WsReloadState};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
services::fs::{ServeDir, ServeFileSystemResponseBody},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
|
||||
/// Sets up and returns a router
|
||||
pub async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<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| async move { Ok(no_cache(file_service_config, response)) })
|
||||
.service(ServeDir::new(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 {
|
||||
router = super::proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::convert::Infallible| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
|
||||
let base_path = format!("/{}", base_path.trim_matches('/'));
|
||||
Router::new()
|
||||
.route(&base_path, axum::routing::any_service(router))
|
||||
.fallback(get(move || {
|
||||
let base_path = base_path.clone();
|
||||
async move { format!("Outside of the base path: {}", base_path) }
|
||||
}))
|
||||
} else {
|
||||
router
|
||||
};
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
fn no_cache(
|
||||
file_service_config: CrateConfig,
|
||||
response: Response<ServeFileSystemResponseBody>,
|
||||
) -> Response<Body> {
|
||||
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Body::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
|
||||
.ok()
|
||||
.unwrap(),
|
||||
);
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.into_response()
|
||||
};
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
|
||||
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
|
||||
response
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Extension(state): Extension<Arc<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| ws_reload_handler(socket, state))
|
||||
}
|
||||
|
||||
async fn ws_reload_handler(mut socket: WebSocket, state: Arc<WsReloadState>) {
|
||||
let mut rx = state.update.subscribe();
|
||||
|
||||
let reload_watcher = tokio::spawn(async move {
|
||||
loop {
|
||||
rx.recv().await.unwrap();
|
||||
|
||||
let _ = socket.send(Message::Text(String::from("reload"))).await;
|
||||
|
||||
// ignore the error
|
||||
println!("forcing reload");
|
||||
|
||||
// flush the errors after recompling
|
||||
rx = rx.resubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
reload_watcher.await.unwrap();
|
||||
}
|
||||
|
||||
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
|
||||
pub 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) = if let Some(true) = web_config.mkcert {
|
||||
// mkcert, use it
|
||||
get_rustls_with_mkcert(web_config)?
|
||||
} else {
|
||||
// if mkcert not specified or false, don't use it
|
||||
get_rustls_without_mkcert(web_config)?
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
RustlsConfig::from_pem_file(cert_path, key_path).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
// 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()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((cert_path, key_path))
|
||||
}
|
||||
|
||||
pub fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
// get paths to cert & key
|
||||
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
|
||||
Ok((cert, key))
|
||||
} else {
|
||||
// missing cert or key
|
||||
Err("https is enabled but cert or key path is missing".into())
|
||||
}
|
||||
}
|
4
packages/cli/tests/fmt.rs
Normal file
4
packages/cli/tests/fmt.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
//! Test that autoformatting works on files/folders/etc
|
||||
|
||||
#[tokio::test]
|
||||
async fn formats() {}
|
|
@ -14,7 +14,7 @@ proc-macro = true
|
|||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
quote = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -14,8 +14,8 @@ proc-macro = true
|
|||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
|
||||
dioxus-rsx = { workspace = true }
|
||||
constcat = "0.3.0"
|
||||
convert_case = "^0.6.0"
|
||||
|
|
|
@ -36,8 +36,9 @@ async fn main() {
|
|||
}
|
||||
|
||||
// Hydrate the page
|
||||
#[cfg(all(feature = "web", not(feature = "server")))]
|
||||
#[cfg(not(feature = "server"))]
|
||||
fn main() {
|
||||
#[cfg(all(feature = "web", not(feature = "server")))]
|
||||
dioxus_web::launch_with_props(
|
||||
dioxus_fullstack::router::RouteWithCfg::<Route>,
|
||||
dioxus_fullstack::prelude::get_root_props_from_document()
|
||||
|
|
|
@ -14,7 +14,7 @@ dioxus-rsx = { workspace = true }
|
|||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, optional = true }
|
||||
|
||||
interprocess-docfix = { version = "1.2.2" }
|
||||
interprocess = { workspace = true }
|
||||
notify = { version = "5.0.0", optional = true }
|
||||
chrono = { version = "0.4.24", default-features = false, features = ["clock"], optional = true }
|
||||
serde_json = "1.0.91"
|
||||
|
|
|
@ -10,7 +10,7 @@ use dioxus_rsx::{
|
|||
hot_reload::{FileMap, FileMapBuildResult, UpdateResult},
|
||||
HotReloadingContext,
|
||||
};
|
||||
use interprocess_docfix::local_socket::LocalSocketListener;
|
||||
use interprocess::local_socket::LocalSocketListener;
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
#[cfg(feature = "file_watcher")]
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
use dioxus_core::Template;
|
||||
#[cfg(feature = "file_watcher")]
|
||||
pub use dioxus_html::HtmlCtx;
|
||||
use interprocess_docfix::local_socket::LocalSocketStream;
|
||||
use interprocess::local_socket::LocalSocketStream;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "custom_file_watcher")]
|
||||
|
@ -15,36 +15,43 @@ mod file_watcher;
|
|||
pub use file_watcher::*;
|
||||
|
||||
/// A message the hot reloading server sends to the client
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(bound(deserialize = "'de: 'static"))]
|
||||
pub enum HotReloadMsg {
|
||||
/// A template has been updated
|
||||
UpdateTemplate(Template),
|
||||
|
||||
/// A template has been updated
|
||||
UpdateAsset(PathBuf),
|
||||
|
||||
/// The program needs to be recompiled, and the client should shut down
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
|
||||
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
|
||||
pub fn connect(mut callback: impl FnMut(HotReloadMsg) + Send + 'static) {
|
||||
std::thread::spawn(move || {
|
||||
let path = PathBuf::from("./").join("target").join("dioxusin");
|
||||
if let Ok(socket) = LocalSocketStream::connect(path) {
|
||||
let mut buf_reader = BufReader::new(socket);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match buf_reader.read_line(&mut buf) {
|
||||
Ok(_) => {
|
||||
let template: HotReloadMsg =
|
||||
serde_json::from_str(Box::leak(buf.into_boxed_str())).unwrap();
|
||||
f(template);
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let socket =
|
||||
LocalSocketStream::connect(path).expect("Could not connect to hot reloading server.");
|
||||
|
||||
let mut buf_reader = BufReader::new(socket);
|
||||
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
|
||||
if let Err(err) = buf_reader.read_line(&mut buf) {
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let template = serde_json::from_str(Box::leak(buf.into_boxed_str())).expect(
|
||||
"Could not parse hot reloading message - make sure your client is up to date",
|
||||
);
|
||||
|
||||
callback(template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ description = "HTML function macros for Dioxus"
|
|||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.66"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
syn = { workspace = true, features = ["full"] }
|
||||
quote = "^1.0.26"
|
||||
convert_case = "^0.6.0"
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ authors = ["Jonathan Kelley", "Evan Almloff"]
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0", features = ["extra-traits", "full"] }
|
||||
quote = "1.0"
|
||||
syn = { workspace = true, features = ["extra-traits", "full"] }
|
||||
quote = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
smallvec = "1.6"
|
||||
|
|
|
@ -15,10 +15,10 @@ keywords = ["dom", "ui", "gui", "react", "router"]
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0", features = ["extra-traits", "full"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0.56"
|
||||
slab = "0.4"
|
||||
syn = { workspace = true, features = ["extra-traits", "full"] }
|
||||
quote = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
slab = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -17,9 +17,9 @@ dioxus-autofmt = { workspace = true }
|
|||
dioxus-rsx = { workspace = true }
|
||||
dioxus-html = { workspace = true, features = ["html-to-rsx"]}
|
||||
html_parser = { workspace = true }
|
||||
proc-macro2 = "1.0.49"
|
||||
quote = "1.0.23"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true, features = ["full"] }
|
||||
convert_case = "0.5.0"
|
||||
|
||||
# [features]
|
||||
|
|
|
@ -13,13 +13,13 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||
quote = { workspace = true }
|
||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||
dioxus-core = { workspace = true, optional = true }
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
quote = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
syn = { workspace = true, features = ["full", "extra-traits"] }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
internment = { version = "0.7.0", optional = true }
|
||||
krates = { version = "0.12.6", optional = true }
|
||||
krates = { version = "0.16.6", optional = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
|
|
@ -29,10 +29,19 @@ pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
|
|||
|
||||
pub struct FileMap<Ctx: HotReloadingContext> {
|
||||
pub map: HashMap<PathBuf, (String, Option<Template>)>,
|
||||
|
||||
in_workspace: HashMap<PathBuf, Option<PathBuf>>,
|
||||
|
||||
phantom: std::marker::PhantomData<Ctx>,
|
||||
}
|
||||
|
||||
struct CachedSynFile {
|
||||
raw: String,
|
||||
file: syn::File,
|
||||
path: PathBuf,
|
||||
template: Option<Template>,
|
||||
}
|
||||
|
||||
impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
||||
/// Create a new FileMap from a crate directory
|
||||
pub fn create(path: PathBuf) -> io::Result<FileMapBuildResult<Ctx>> {
|
||||
|
@ -106,34 +115,40 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
|||
let mut file = File::open(file_path)?;
|
||||
let mut src = String::new();
|
||||
file.read_to_string(&mut src)?;
|
||||
if let Ok(syntax) = syn::parse_file(&src) {
|
||||
let in_workspace = self.child_in_workspace(crate_dir)?;
|
||||
if let Some((old_src, template_slot)) = self.map.get_mut(file_path) {
|
||||
if let Ok(old) = syn::parse_file(old_src) {
|
||||
match find_rsx(&syntax, &old) {
|
||||
DiffResult::CodeChanged => {
|
||||
self.map.insert(file_path.to_path_buf(), (src, None));
|
||||
}
|
||||
DiffResult::RsxChanged(changed) => {
|
||||
let mut messages: Vec<Template> = Vec::new();
|
||||
for (old, new) in changed.into_iter() {
|
||||
let old_start = old.span().start();
|
||||
|
||||
if let (Ok(old_call_body), Ok(new_call_body)) = (
|
||||
syn::parse2::<CallBody>(old.tokens),
|
||||
syn::parse2::<CallBody>(new),
|
||||
) {
|
||||
// if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
|
||||
// we need to check if the file is in a workspace or not and strip the prefix accordingly
|
||||
let prefix = if let Some(workspace) = &in_workspace {
|
||||
workspace
|
||||
} else {
|
||||
crate_dir
|
||||
};
|
||||
if let Ok(file) = file_path.strip_prefix(prefix) {
|
||||
let line = old_start.line;
|
||||
let column = old_start.column + 1;
|
||||
let location = file.display().to_string()
|
||||
// If we can't parse the contents we want to pass it off to the build system to tell the user that there's a syntax error
|
||||
let Ok(syntax) = syn::parse_file(&src) else {
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
};
|
||||
|
||||
let in_workspace = self.child_in_workspace(crate_dir)?;
|
||||
|
||||
if let Some((old_src, template_slot)) = self.map.get_mut(file_path) {
|
||||
if let Ok(old) = syn::parse_file(old_src) {
|
||||
match find_rsx(&syntax, &old) {
|
||||
DiffResult::CodeChanged => {
|
||||
self.map.insert(file_path.to_path_buf(), (src, None));
|
||||
}
|
||||
DiffResult::RsxChanged(changed) => {
|
||||
let mut messages: Vec<Template> = Vec::new();
|
||||
for (old, new) in changed.into_iter() {
|
||||
let old_start = old.span().start();
|
||||
|
||||
if let (Ok(old_call_body), Ok(new_call_body)) = (
|
||||
syn::parse2::<CallBody>(old.tokens),
|
||||
syn::parse2::<CallBody>(new),
|
||||
) {
|
||||
// if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
|
||||
// we need to check if the file is in a workspace or not and strip the prefix accordingly
|
||||
let prefix = if let Some(workspace) = &in_workspace {
|
||||
workspace
|
||||
} else {
|
||||
crate_dir
|
||||
};
|
||||
if let Ok(file) = file_path.strip_prefix(prefix) {
|
||||
let line = old_start.line;
|
||||
let column = old_start.column + 1;
|
||||
let location = file.display().to_string()
|
||||
+ ":"
|
||||
+ &line.to_string()
|
||||
+ ":"
|
||||
|
@ -141,45 +156,42 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
|
|||
// the byte index doesn't matter, but dioxus needs it
|
||||
+ ":0";
|
||||
|
||||
if let Some(template) = new_call_body
|
||||
.update_template::<Ctx>(
|
||||
Some(old_call_body),
|
||||
Box::leak(location.into_boxed_str()),
|
||||
)
|
||||
{
|
||||
// dioxus cannot handle empty templates
|
||||
if template.roots.is_empty() {
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
} else {
|
||||
// if the template is the same, don't send it
|
||||
if let Some(old_template) = template_slot {
|
||||
if old_template == &template {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
*template_slot = Some(template);
|
||||
messages.push(template);
|
||||
}
|
||||
} else {
|
||||
if let Some(template) = new_call_body.update_template::<Ctx>(
|
||||
Some(old_call_body),
|
||||
Box::leak(location.into_boxed_str()),
|
||||
) {
|
||||
// dioxus cannot handle empty templates
|
||||
if template.roots.is_empty() {
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
} else {
|
||||
// if the template is the same, don't send it
|
||||
if let Some(old_template) = template_slot {
|
||||
if old_template == &template {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
*template_slot = Some(template);
|
||||
messages.push(template);
|
||||
}
|
||||
} else {
|
||||
return Ok(UpdateResult::NeedsRebuild);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(UpdateResult::UpdatedRsx(messages));
|
||||
}
|
||||
return Ok(UpdateResult::UpdatedRsx(messages));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if this is a new file, rebuild the project
|
||||
let FileMapBuildResult { map, mut errors } =
|
||||
FileMap::create(crate_dir.to_path_buf())?;
|
||||
if let Some(err) = errors.pop() {
|
||||
return Err(err);
|
||||
}
|
||||
*self = map;
|
||||
}
|
||||
} else {
|
||||
// if this is a new file, rebuild the project
|
||||
let FileMapBuildResult { map, mut errors } = FileMap::create(crate_dir.to_path_buf())?;
|
||||
if let Some(err) = errors.pop() {
|
||||
return Err(err);
|
||||
}
|
||||
*self = map;
|
||||
}
|
||||
|
||||
Ok(UpdateResult::NeedsRebuild)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ description = "Server function macros for Dioxus"
|
|||
[dependencies]
|
||||
proc-macro2 = "^1.0.63"
|
||||
quote = "^1.0.26"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
syn = { workspace = true, features = ["full"] }
|
||||
convert_case = "^0.6.0"
|
||||
server_fn_macro = "^0.6.5"
|
||||
|
||||
|
|
Loading…
Reference in a new issue