Create a Static Site Generation platform; Deduplicate hot reloading code (#2226)

* create static site generation helpers in the router crate

* work on integrating static site generation into fullstack

* move ssg into a separate crate

* integrate ssg with the launch builder

* simplify ssg example

* fix static_routes for child routes

* move CLI hot reloading websocket code into dioxus-hot-reload

* fix some unused imports

* use the same hot reloading websocket code for fullstack

* fix fullstack hot reloading

* move cli hot reloading logic into the hot reload crate

* ssg example working with dx serve

* add more examples

* fix clippy

* fix formatting

* fix hot reload doctest imports

* fix axum imports

* don't run server doc tests

* Fix hot reload websocket doc examples
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-05-22 07:24:15 -05:00
1 parent 460b70e0f0
commit 245003a5d4
59 files changed
+1507 -791

No files matched your search

+20 -109
View File
@@ -15,9 +15,11 @@
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
//! axum::Server::bind(&addr)
//! .serve(
//! let listener = tokio::net::TcpListener::bind("127.0.0.01:8080")
//! .await
//! .unwrap();
//! axum::serve(
//! listener,
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .serve_dioxus_application("", ServerConfig::new(app, ()))
@@ -91,30 +93,6 @@ pub trait DioxusRouterExt<S> {
/// ```
fn register_server_fns(self) -> Self;
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// hot_reload_init!();
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Connect to hot reloading in debug mode
/// .connect_hot_reload()
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn connect_hot_reload(self) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
@@ -151,7 +129,7 @@ pub trait DioxusRouterExt<S> {
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.
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
///
/// # Example
/// ```rust
@@ -263,44 +241,22 @@ where
let ssr_state = SSRState::new(&cfg);
// Add server functions and render index.html
self.serve_static_assets(cfg.assets_path.clone())
let mut server = 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,
)))
}
}
.register_server_fns();
fn connect_hot_reload(self) -> Self {
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
self.nest(
"/_dioxus",
Router::new()
.route(
"/ws",
get(|ws: axum::extract::WebSocketUpgrade| async {
ws.on_upgrade(|mut ws| async move {
use axum::extract::ws::Message;
let _ = ws.send(Message::Text("connected".into())).await;
loop {
if ws.recv().await.is_none() {
break;
}
}
})
}),
)
.route("/hot_reload", get(hot_reload_handler)),
)
}
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
{
self
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
use dioxus_hot_reload::HotReloadRouterExt;
server = server.forward_cli_hot_reloading();
}
server.fallback(get(render_handler).with_state((
cfg,
Arc::new(build_virtual_dom),
ssr_state,
)))
}
}
}
@@ -424,51 +380,6 @@ fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
.unwrap()
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload"))]
pub async fn hot_reload_handler(ws: axum::extract::WebSocketUpgrade) -> impl IntoResponse {
use axum::extract::ws::Message;
use futures_util::StreamExt;
let state = crate::hot_reload::spawn_hot_reload().await;
ws.on_upgrade(move |mut socket| async move {
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx =
tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver.clone());
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if socket.send(Message::Text(template)).await.is_err() {
break;
};
}
}
println!("😳 Hot Reload WebSocket disconnected");
})
}
/// A handler for Dioxus server functions. This will run the server function and return the result.
async fn handle_server_fns_inner(
path: &str,
+12 -6
View File
@@ -131,15 +131,21 @@ impl Config {
let ssr_state = SSRState::new(&cfg);
let router = axum::Router::new().register_server_fns();
#[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((
let router = {
let mut router = router.serve_static_assets(cfg.assets_path.clone()).await;
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
use dioxus_hot_reload::HotReloadRouterExt;
router = router.forward_cli_hot_reloading();
}
router.fallback(get(render_handler).with_state((
cfg,
Arc::new(build_virtual_dom),
ssr_state,
)));
)))
};
let router = router.into_make_service();
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, router).await.unwrap();
-62
View File
@@ -1,62 +0,0 @@
use std::sync::Arc;
use dioxus_lib::prelude::Template;
use tokio::sync::{
watch::{channel, Receiver},
RwLock,
};
#[derive(Clone)]
pub struct HotReloadState {
// The cache of all templates that have been modified since the last time we checked
pub(crate) templates: Arc<RwLock<std::collections::HashSet<dioxus_lib::prelude::Template>>>,
// The channel to send messages to the hot reload thread
pub(crate) message_receiver: Receiver<Option<Template>>,
}
impl Default for HotReloadState {
fn default() -> Self {
let templates = Arc::new(RwLock::new(std::collections::HashSet::new()));
let (tx, rx) = channel(None);
dioxus_hot_reload::connect({
let templates = templates.clone();
move |msg| match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
{
let mut templates = templates.blocking_write();
templates.insert(template);
}
if let Err(err) = tx.send(Some(template)) {
tracing::error!("Failed to send hot reload message: {}", err);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
_ => {}
}
});
Self {
templates,
message_receiver: rx,
}
}
}
// Hot reloading can be expensive to start so we spawn a new thread
static HOT_RELOAD_STATE: tokio::sync::OnceCell<HotReloadState> = tokio::sync::OnceCell::const_new();
pub(crate) async fn spawn_hot_reload() -> &'static HotReloadState {
HOT_RELOAD_STATE
.get_or_init(|| async {
println!("spinning up hot reloading");
let r = tokio::task::spawn_blocking(HotReloadState::default)
.await
.unwrap();
println!("hot reloading ready");
r
})
.await
}
@@ -57,23 +57,3 @@ static SERVER_DATA: once_cell::sync::Lazy<Option<HTMLDataCursor>> =
pub(crate) fn take_server_data<T: DeserializeOwned>() -> Option<T> {
SERVER_DATA.as_ref()?.take()
}
#[cfg(not(feature = "server"))]
/// Get the props from the document. This is only available in the browser.
///
/// When dioxus-fullstack renders the page, it will serialize the root props and put them in the document. This function gets them from the document.
pub fn get_root_props_from_document<T: DeserializeOwned>() -> Option<T> {
#[cfg(all(feature = "web", target_arch = "wasm32"))]
{
let attribute = web_sys::window()?
.document()?
.get_element_by_id("dioxus-storage-props")?
.get_attribute("data-serialized")?;
serde_from_bytes(attribute.as_bytes())
}
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
{
None
}
}
@@ -14,19 +14,6 @@ pub(crate) fn serde_to_writable<T: Serialize>(
Ok(())
}
#[cfg(feature = "server")]
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
pub(crate) fn encode_props_in_element<T: Serialize>(
data: &T,
write_to: &mut impl std::io::Write,
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
write_to.write_all(
r#"<meta hidden="true" id="dioxus-storage-props" data-serialized=""#.as_bytes(),
)?;
serde_to_writable(data, write_to)?;
Ok(write_to.write_all(r#"" />"#.as_bytes())?)
}
#[cfg(feature = "server")]
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
pub(crate) fn encode_in_element(
+5 -29
View File
@@ -8,21 +8,14 @@ pub use once_cell;
mod html_storage;
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
mod axum_adapter;
mod config;
mod hooks;
pub mod launch;
#[cfg(all(
debug_assertions,
feature = "hot-reload",
feature = "server",
not(target_arch = "wasm32")
))]
mod hot_reload;
pub use config::*;
#[cfg(feature = "server")]
@@ -43,17 +36,9 @@ pub mod prelude {
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
pub use crate::axum_adapter::*;
#[cfg(not(feature = "server"))]
#[cfg_attr(docsrs, doc(cfg(not(feature = "server"))))]
pub use crate::html_storage::deserialize::get_root_props_from_document;
#[cfg(all(feature = "server", feature = "router"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "router"))))]
pub use crate::render::pre_cache_static_routes_with_props;
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::render::SSRState;
pub use crate::render::{FullstackHTMLTemplate, SSRState};
#[cfg(feature = "router")]
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
@@ -70,23 +55,14 @@ pub mod prelude {
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use crate::server_context::{
extract, server_context, DioxusServerContext, FromServerContext, ProvideServerContext,
extract, server_context, with_server_context, DioxusServerContext, FromServerContext,
ProvideServerContext,
};
#[cfg(feature = "server")]
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
pub use dioxus_ssr::incremental::{IncrementalRenderer, IncrementalRendererConfig};
pub use dioxus_server_macro::*;
pub use server_fn::{self, ServerFn as _, ServerFnError};
}
// // Warn users about overlapping features
// #[cfg(all(feature = "server", feature = "web", not(doc)))]
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
// #[cfg(all(feature = "server", feature = "desktop", not(doc)))]
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
// #[cfg(all(feature = "server", feature = "mobile", not(doc)))]
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `mobile` feature are overlapping. Please choose one or the other.");
+17 -41
View File
@@ -1,5 +1,5 @@
//! A shared pool of renderers for efficient server side rendering.
use crate::{render::dioxus_core::NoOpMutations, server_context::with_server_context};
use crate::render::dioxus_core::NoOpMutations;
use dioxus_ssr::{
incremental::{RenderFreshness, WrapBody},
Renderer,
@@ -45,7 +45,7 @@ impl SsrRendererPool {
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
server_context: &DioxusServerContext,
) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> {
let wrapper = FullstackRenderer {
let wrapper = FullstackHTMLTemplate {
cfg: cfg.clone(),
server_context: server_context.clone(),
};
@@ -210,12 +210,24 @@ impl SSRState {
}
}
struct FullstackRenderer {
/// The template that wraps the body of the HTML for a fullstack page. This template contains the data needed to hydrate server functions that were run on the server.
#[derive(Default)]
pub struct FullstackHTMLTemplate {
cfg: ServeConfig,
server_context: DioxusServerContext,
}
impl dioxus_ssr::incremental::WrapBody for FullstackRenderer {
impl FullstackHTMLTemplate {
/// Create a new [`FullstackHTMLTemplate`].
pub fn new(cfg: &ServeConfig, server_context: &DioxusServerContext) -> Self {
Self {
cfg: cfg.clone(),
server_context: server_context.clone(),
}
}
}
impl dioxus_ssr::incremental::WrapBody for FullstackHTMLTemplate {
fn render_before_body<R: std::io::Write>(
&self,
to: &mut R,
@@ -231,11 +243,6 @@ impl dioxus_ssr::incremental::WrapBody for FullstackRenderer {
&self,
to: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
// serialize the props
// TODO: restore props serialization
// crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to).map_err(
// |err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)),
// )?;
// serialize the server state
crate::html_storage::serialize::encode_in_element(
&*self.server_context.html_data().map_err(|_| {
@@ -263,38 +270,7 @@ impl dioxus_ssr::incremental::WrapBody for FullstackRenderer {
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
// In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
// This is copied from the Dioxus-CLI package at ../packages/cli
let disconnect_js = r#"(function () {
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
var url = protocol + "//" + window.location.host + "/_dioxus/ws";
var poll_interval = 8080;
var reload_upon_connect = (event) => {
// Firefox will send a 1001 code when the connection is closed because the page is reloaded
// Only firefox will trigger the onclose event when the page is reloaded manually: https://stackoverflow.com/questions/10965720/should-websocket-onclose-be-triggered-by-user-navigation-or-refresh
// We should not reload the page in this case
if (event.code === 1001) {
return;
}
window.setTimeout(() => {
var ws = new WebSocket(url);
ws.onopen = () => window.location.reload();
ws.onclose = reload_upon_connect;
}, poll_interval);
};
var ws = new WebSocket(url);
ws.onmessage = (ev) => {
console.log("Received message: ", ev, ev.data);
if (ev.data == "reload") {
window.location.reload();
}
};
ws.onclose = reload_upon_connect;
})();"#;
let disconnect_js = dioxus_hot_reload::RECONNECT_SCRIPT;
to.write_all(r#"<script>"#.as_bytes())?;
to.write_all(disconnect_js.as_bytes())?;
+6 -20
View File
@@ -16,26 +16,6 @@ pub struct ServeConfigBuilder {
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
}
/// A template for incremental rendering that does nothing.
#[derive(Default, Clone)]
pub struct EmptyIncrementalRenderTemplate;
impl dioxus_ssr::incremental::WrapBody for EmptyIncrementalRenderTemplate {
fn render_after_body<R: std::io::Write>(
&self,
_: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
Ok(())
}
fn render_before_body<R: std::io::Write>(
&self,
_: &mut R,
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
Ok(())
}
}
impl ServeConfigBuilder {
/// Create a new ServeConfigBuilder with the root component and props to render on the server.
pub fn new() -> Self {
@@ -151,6 +131,12 @@ pub struct ServeConfig {
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
}
impl Default for ServeConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl ServeConfig {
/// Create a new builder for a ServeConfig
pub fn builder() -> ServeConfigBuilder {
+2 -1
View File
@@ -159,7 +159,8 @@ pub async fn extract<E: FromServerContext<I>, I>() -> Result<E, E::Rejection> {
E::from_request(&server_context()).await
}
pub(crate) fn with_server_context<O>(context: DioxusServerContext, f: impl FnOnce() -> O) -> O {
/// Run a function inside of the server context.
pub fn with_server_context<O>(context: DioxusServerContext, f: impl FnOnce() -> O) -> O {
// before polling the future, we need to set the context
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(Box::new(context)));
// poll the future, which may call server_context()