diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index e419205a9..98eac307b 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -17,7 +17,8 @@ dioxus_server_macro = { workspace = true } # axum axum = { workspace = true, features = ["ws", "macros"], optional = true } -tower-http = { workspace = true, optional = true, features = ["fs", "compression-gzip"] } +tower-http = { workspace = true, optional = true, features = ["fs"] } +async-compression = { version = "0.4.6", features = ["gzip", "tokio"], optional = true } dioxus-lib = { workspace = true } @@ -73,7 +74,7 @@ desktop = ["dioxus-desktop"] mobile = ["dioxus-mobile"] default-tls = ["server_fn/default-tls"] rustls = ["server_fn/rustls"] -axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"] +axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "async-compression"] server = [ "server_fn/ssr", "dioxus_server_macro/server", diff --git a/packages/fullstack/examples/axum-auth/src/main.rs b/packages/fullstack/examples/axum-auth/src/main.rs index e1638e1b6..7410da416 100644 --- a/packages/fullstack/examples/axum-auth/src/main.rs +++ b/packages/fullstack/examples/axum-auth/src/main.rs @@ -52,6 +52,7 @@ fn main() { .serve_dioxus_application(ServeConfig::builder().build(), || { VirtualDom::new(app) }) + .await .layer( axum_session_auth::AuthSessionLayer::< crate::auth::User, diff --git a/packages/fullstack/src/assets.rs b/packages/fullstack/src/assets.rs new file mode 100644 index 000000000..5dd27da3e --- /dev/null +++ b/packages/fullstack/src/assets.rs @@ -0,0 +1,70 @@ +//! Handles pre-compression for any static assets + +use std::{ffi::OsString, path::PathBuf, pin::Pin}; + +use async_compression::tokio::bufread::GzipEncoder; +use futures_util::Future; +use tokio::task::JoinSet; + +#[allow(unused)] +pub async fn pre_compress_files(directory: PathBuf) -> tokio::io::Result<()> { + // print to stdin encoded gzip data + pre_compress_dir(directory).await?; + Ok(()) +} + +fn pre_compress_dir( + path: PathBuf, +) -> Pin> + Send + Sync>> { + Box::pin(async move { + let mut entries = tokio::fs::read_dir(&path).await?; + let mut set: JoinSet> = JoinSet::new(); + + while let Some(entry) = entries.next_entry().await? { + set.spawn(async move { + if entry.file_type().await?.is_dir() { + if let Err(err) = pre_compress_dir(entry.path()).await { + tracing::error!( + "Failed to pre-compress directory {}: {}", + entry.path().display(), + err + ); + } + } else if let Err(err) = pre_compress_file(entry.path()).await { + tracing::error!( + "Failed to pre-compress static assets {}: {}", + entry.path().display(), + err + ); + } + + Ok(()) + }); + } + while let Some(res) = set.join_next().await { + res??; + } + Ok(()) + }) +} + +async fn pre_compress_file(path: PathBuf) -> tokio::io::Result<()> { + let file = tokio::fs::File::open(&path).await?; + let stream = tokio::io::BufReader::new(file); + let mut encoder = GzipEncoder::new(stream); + let new_extension = match path.extension() { + Some(ext) => { + if ext.to_string_lossy().to_lowercase().ends_with("gz") { + return Ok(()); + } + let mut ext = ext.to_os_string(); + ext.push(".gz"); + ext + } + None => OsString::from("gz"), + }; + let output = path.with_extension(new_extension); + let mut buffer = tokio::fs::File::create(&output).await?; + tokio::io::copy(&mut encoder, &mut buffer).await?; + Ok(()) +} diff --git a/packages/fullstack/src/axum_adapter.rs b/packages/fullstack/src/axum_adapter.rs index cc23d5345..dcafdb01d 100644 --- a/packages/fullstack/src/axum_adapter.rs +++ b/packages/fullstack/src/axum_adapter.rs @@ -64,6 +64,7 @@ use axum::{ Router, }; use dioxus_lib::prelude::VirtualDom; +use futures_util::Future; use http::header::*; use std::sync::Arc; @@ -149,7 +150,12 @@ pub trait DioxusRouterExt { /// unimplemented!() /// } /// ``` - fn serve_static_assets(self, assets_path: impl Into) -> Self; + fn serve_static_assets( + self, + assets_path: impl Into, + ) -> impl Future + Send + Sync + where + Self: Sized; /// Serves the Dioxus application. This will serve a complete server side rendered application. /// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading. @@ -182,7 +188,9 @@ pub trait DioxusRouterExt { self, cfg: impl Into, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, - ) -> Self; + ) -> impl Future + Send + Sync + where + Self: Sized; } impl DioxusRouterExt for Router @@ -206,59 +214,75 @@ where self } - fn serve_static_assets(mut self, assets_path: impl Into) -> Self { + fn serve_static_assets( + mut self, + assets_path: impl Into, + ) -> impl Future + Send + Sync { use tower_http::services::{ServeDir, ServeFile}; let assets_path = assets_path.into(); - - // Serve all files in dist folder except index.html - let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| { - panic!( - "Couldn't read assets directory at {:?}: {}", - &assets_path, e - ) - }); - - for entry in dir.flatten() { - let path = entry.path(); - if path.ends_with("index.html") { - continue; + async move { + #[cfg(not(debug_assertions))] + if let Err(err) = crate::assets::pre_compress_files(assets_path.clone()).await { + tracing::error!("Failed to pre-compress static assets: {}", err); } - let route = path - .strip_prefix(&assets_path) - .unwrap() - .iter() - .map(|segment| { - segment.to_str().unwrap_or_else(|| { - panic!("Failed to convert path segment {:?} to string", segment) + + // Serve all files in dist folder except index.html + let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| { + panic!( + "Couldn't read assets directory at {:?}: {}", + &assets_path, e + ) + }); + + for entry in dir.flatten() { + let path = entry.path(); + if path.ends_with("index.html") { + continue; + } + let route = path + .strip_prefix(&assets_path) + .unwrap() + .iter() + .map(|segment| { + segment.to_str().unwrap_or_else(|| { + panic!("Failed to convert path segment {:?} to string", segment) + }) }) - }) - .collect::>() - .join("/"); - let route = format!("/{}", route); - if path.is_dir() { - self = self.nest_service(&route, ServeDir::new(path)); - } else { - self = self.nest_service(&route, ServeFile::new(path)); + .collect::>() + .join("/"); + let route = format!("/{}", route); + if path.is_dir() { + self = self.nest_service(&route, ServeDir::new(path).precompressed_gzip()); + } else { + self = self.nest_service(&route, ServeFile::new(path).precompressed_gzip()); + } } - } - self + self + } } fn serve_dioxus_application( self, cfg: impl Into, build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static, - ) -> Self { + ) -> impl Future + Send + Sync { let cfg = cfg.into(); - let ssr_state = SSRState::new(&cfg); + async move { + let ssr_state = SSRState::new(&cfg); - // Add server functions and render index.html - self.serve_static_assets(cfg.assets_path.clone()) - .connect_hot_reload() - .register_server_fns() - .fallback(get(render_handler).with_state((cfg, Arc::new(build_virtual_dom), ssr_state))) + // Add server functions and render index.html + self.serve_static_assets(cfg.assets_path.clone()) + .await + .connect_hot_reload() + .register_server_fns() + .fallback(get(render_handler).with_state(( + cfg, + Arc::new(build_virtual_dom), + ssr_state, + ))) + } } fn connect_hot_reload(self) -> Self { @@ -472,7 +496,6 @@ async fn handle_server_fns_inner( if let Some(mut service) = server_fn::axum::get_server_fn_service(&path_string) { - let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts))); additional_context(); @@ -488,7 +511,6 @@ async fn handle_server_fns_inner( // actually run the server fn let mut res = service.run(req).await; - // it it accepts text/html (i.e., is a plain form post) and doesn't already have a // Location set, then redirect to Referer if accepts_html { diff --git a/packages/fullstack/src/config.rs b/packages/fullstack/src/config.rs index a0f7211c6..3666665da 100644 --- a/packages/fullstack/src/config.rs +++ b/packages/fullstack/src/config.rs @@ -133,18 +133,14 @@ impl Config { #[cfg(not(any(feature = "desktop", feature = "mobile")))] let router = router .serve_static_assets(cfg.assets_path.clone()) + .await .connect_hot_reload() .fallback(get(render_handler).with_state(( cfg, Arc::new(build_virtual_dom), ssr_state, ))); - let router = router - .layer( - ServiceBuilder::new() - .layer(tower_http::compression::CompressionLayer::new().gzip(true)), - ) - .into_make_service(); + let router = router.into_make_service(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, router).await.unwrap(); } diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 31090670e..ada165830 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -8,6 +8,8 @@ pub use once_cell; mod html_storage; +#[cfg(feature = "axum")] +mod assets; #[cfg_attr(docsrs, doc(cfg(feature = "axum")))] #[cfg(feature = "axum")] mod axum_adapter; diff --git a/packages/signals/src/reactive_context.rs b/packages/signals/src/reactive_context.rs index b4062be78..b8c5e27d3 100644 --- a/packages/signals/src/reactive_context.rs +++ b/packages/signals/src/reactive_context.rs @@ -5,7 +5,7 @@ use futures_channel::mpsc::UnboundedReceiver; use generational_box::SyncStorage; use std::{cell::RefCell, hash::Hash}; -use crate::{CopyValue, Readable, Writable}; +use crate::{CopyValue, Writable}; /// A context for signal reads and writes to be directed to /// @@ -26,6 +26,7 @@ impl std::fmt::Display for ReactiveContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[cfg(debug_assertions)] { + use crate::Readable; if let Ok(read) = self.inner.try_read() { return write!(f, "ReactiveContext created at {}", read.origin); } @@ -58,7 +59,7 @@ impl ReactiveContext { pub fn new_with_callback( callback: impl FnMut() + Send + Sync + 'static, scope: ScopeId, - origin: &'static std::panic::Location<'static>, + #[allow(unused)] origin: &'static std::panic::Location<'static>, ) -> Self { let inner = Inner { self_: None,