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

@@ -0,0 +1,31 @@
(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;
})();
+9
View File
@@ -14,6 +14,15 @@ mod file_watcher;
#[cfg(feature = "custom_file_watcher")]
pub use file_watcher::*;
#[cfg(feature = "serve")]
mod websocket;
#[cfg(feature = "serve")]
pub use websocket::*;
#[cfg(feature = "serve")]
/// The script to inject into the page to reconnect to server if the connection is lost
pub const RECONNECT_SCRIPT: &str = include_str!("assets/autoreload.js");
/// A message the hot reloading server sends to the client
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(bound(deserialize = "'de: 'static"))]
+279
View File
@@ -0,0 +1,279 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use axum::{
extract::{
ws::{Message, WebSocket},
WebSocketUpgrade,
},
response::IntoResponse,
routing::get,
Extension, Router,
};
use dioxus_core::Template;
use futures_util::{pin_mut, FutureExt};
use tokio::sync::broadcast;
use crate::HotReloadMsg;
/// A extension trait with utilities for integrating Dioxus hot reloading with your Axum router.
pub trait HotReloadRouterExt<S> {
/// 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, no_run
/// #![allow(non_snake_case)]
/// use dioxus_hot_reload::{HotReloadReceiver, HotReloadRouterExt};
/// use axum::Extension;
///
/// #[tokio::main]
/// async fn main() {
/// let listener = tokio::net::TcpListener::bind("127.0.0.01:8080")
/// .await
/// .unwrap();
/// let ws_reload = HotReloadReceiver::default();
/// axum::serve(
/// listener,
/// axum::Router::new()
/// // Connect to hot reloading in debug mode
/// .connect_hot_reload()
/// .layer(Extension(ws_reload))
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn connect_hot_reload(self) -> Self;
/// Like [`connect_hot_reload`] but connects to the hot reloading messages that the CLI sends in the desktop and fullstack platforms
///
/// # Example
/// ```rust, no_run
/// #![allow(non_snake_case)]
/// use dioxus_hot_reload::{HotReloadReceiver, HotReloadRouterExt};
///
/// #[tokio::main]
/// async fn main() {
/// let listener = tokio::net::TcpListener::bind("127.0.0.01:8080")
/// .await
/// .unwrap();
/// let ws_reload = HotReloadReceiver::default();
/// axum::serve(
/// listener,
/// axum::Router::new()
/// // Connect to hot reloading in debug mode
/// .forward_cli_hot_reloading()
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn forward_cli_hot_reloading(self) -> Self;
}
impl<S> HotReloadRouterExt<S> for Router<S>
where
S: Clone + Send + Sync + 'static,
{
fn connect_hot_reload(self) -> Self {
self.nest(
"/_dioxus",
Router::new()
.route("/ws", get(ws_handler))
.route("/hot_reload", get(hot_reload_handler)),
)
}
fn forward_cli_hot_reloading(mut self) -> Self {
static HOT_RELOAD_STATE: once_cell::sync::Lazy<HotReloadReceiver> =
once_cell::sync::Lazy::new(forward_cli_hot_reload);
let hot_reload_state: HotReloadReceiver = HOT_RELOAD_STATE.clone();
self = self.connect_hot_reload().layer(Extension(hot_reload_state));
self
}
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<HotReloadReceiver>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| ws_reload_handler(socket, state))
}
async fn ws_reload_handler(mut socket: WebSocket, state: HotReloadReceiver) {
let mut rx = state.reload.subscribe();
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();
}
}
/// State that is shared between the websocket and the hot reloading watcher
#[derive(Clone)]
pub struct HotReloadReceiver {
/// Hot reloading messages sent from the client
// NOTE: We use a send broadcast channel to allow clones
messages: broadcast::Sender<HotReloadMsg>,
/// Rebuilds sent from the client
reload: broadcast::Sender<()>,
/// Any template updates that have happened since the last full render
template_updates: SharedTemplateUpdates,
}
impl HotReloadReceiver {
/// Create a new [`HotReloadReceiver`]
pub fn new() -> Self {
Self::default()
}
}
impl Default for HotReloadReceiver {
fn default() -> Self {
Self {
messages: broadcast::channel(100).0,
reload: broadcast::channel(100).0,
template_updates: Default::default(),
}
}
}
type SharedTemplateUpdates = Arc<Mutex<HashMap<&'static str, Template>>>;
impl HotReloadReceiver {
/// Find all templates that have been updated since the last full render
pub fn all_modified_templates(&self) -> Vec<Template> {
self.template_updates
.lock()
.unwrap()
.values()
.cloned()
.collect()
}
/// Send a hot reloading message to the client
pub fn send_message(&self, msg: HotReloadMsg) {
// Before we send the message, update the list of changed templates
if let HotReloadMsg::UpdateTemplate(template) = msg {
let mut template_updates = self.template_updates.lock().unwrap();
template_updates.insert(template.name, template);
}
if let Err(err) = self.messages.send(msg) {
tracing::error!("Failed to send hot reload message: {}", err);
}
}
/// Subscribe to hot reloading messages
pub fn subscribe(&self) -> broadcast::Receiver<HotReloadMsg> {
self.messages.subscribe()
}
/// Reload the website
pub fn reload(&self) {
self.reload.send(()).unwrap();
}
}
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<HotReloadReceiver>,
) -> impl IntoResponse {
ws.on_upgrade(|socket| async move {
let err = hotreload_loop(socket, state).await;
if let Err(err) = err {
tracing::error!("Hotreload receiver failed: {}", err);
}
})
}
async fn hotreload_loop(
mut socket: WebSocket,
state: HotReloadReceiver,
) -> Result<(), axum::Error> {
tracing::info!("🔥 Hot Reload WebSocket connected");
let mut rx = state.messages.subscribe();
// update any rsx calls that changed before the websocket connected.
// These templates will be sent down immediately so the page is in sync with the hotreloaded version
// The compiled version will be different from the one we actually want to present
for template in state.all_modified_templates() {
socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await?;
}
loop {
let msg = {
// Poll both the receiver and the socket
//
// This shuts us down if the connection is closed.
let mut _socket = socket.recv().fuse();
let mut _rx = rx.recv().fuse();
pin_mut!(_socket, _rx);
let msg = futures_util::select! {
msg = _rx => msg,
e = _socket => {
if let Some(Err(e)) = e {
tracing::info!("🔥 Hot Reload WebSocket Error: {}", e);
} else {
tracing::info!("🔥 Hot Reload WebSocket Closed");
}
break;
},
};
let Ok(msg) = msg else { break };
match msg {
HotReloadMsg::UpdateTemplate(template) => {
Message::Text(serde_json::to_string(&template).unwrap())
}
HotReloadMsg::UpdateAsset(asset) => {
Message::Text(format!("reload-asset: {}", asset.display()))
}
HotReloadMsg::Shutdown => {
tracing::info!("🔥 Hot Reload WebSocket shutting down");
break;
}
}
};
socket.send(msg).await?;
}
Ok(())
}
pub(crate) fn forward_cli_hot_reload() -> HotReloadReceiver {
let hot_reload_state = HotReloadReceiver::default();
// Hot reloading can be expensive to start so we spawn a new thread
std::thread::spawn({
let hot_reload_state = hot_reload_state.clone();
move || {
crate::connect(move |msg| hot_reload_state.send_message(msg));
}
});
hot_reload_state
}