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

-36
View File
@@ -1,36 +0,0 @@
// Dioxus-CLI
// https://github.com/DioxusLabs/dioxus/tree/master/packages/cli
// NOTE: This is also used in the fullstack package at ../packages/fullstack/src/render.rs, if you make changes here, make sure to update the version in there as well
// TODO: Extract hot reloading with axum into a separate crate or use the fullstack hot reloading Axum extension trait here
(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;
})();
+1 -4
View File
@@ -564,10 +564,7 @@ pub fn gen_page(config: &CrateConfig, manifest: Option<&AssetManifest>, serve: b
replace_or_insert_before("{script_include}", &script_str, "</body", &mut html);
if serve {
html += &format!(
"<script>{}</script>",
include_str!("./assets/autoreload.js")
);
html += &format!("<script>{}</script>", dioxus_hot_reload::RECONNECT_SCRIPT);
}
let base_path = match &config.dioxus_config.web.app.base_path {
+1 -1
View File
@@ -65,7 +65,7 @@ impl Build {
// argument is explicitly set to `None`.
crate::builder::build_desktop(&crate_config, false, self.build.skip_assets, None)?
}
Platform::Fullstack => {
Platform::Fullstack | Platform::StaticGeneration => {
// Fullstack mode must be built with web configs on the desktop
// (server) binary as well as the web binary
let _config = AssetConfigDropGuard::new();
+5 -3
View File
@@ -3,7 +3,7 @@ use manganis_cli_support::AssetManifest;
use super::*;
use cargo_toml::Dependency::{Detailed, Inherited, Simple};
use std::{fs::create_dir_all, io::Write, path::PathBuf};
use std::fs::create_dir_all;
/// Run the WASM project on dev-server
#[derive(Clone, Debug, Parser)]
@@ -18,7 +18,7 @@ impl Serve {
let mut crate_config = dioxus_cli_config::CrateConfig::new(bin)?;
let serve_cfg = self.serve.clone();
// change the relase state.
// change the release state.
let hot_reload = self.serve.hot_reload || crate_config.dioxus_config.application.hot_reload;
crate_config.with_hot_reload(hot_reload);
crate_config.with_cross_origin_policy(self.serve.cross_origin_policy);
@@ -67,7 +67,9 @@ impl Serve {
match platform {
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?,
Platform::Fullstack | Platform::StaticGeneration => {
fullstack::startup(crate_config.clone(), &serve_cfg).await?
}
_ => unreachable!(),
}
+17 -15
View File
@@ -1,3 +1,4 @@
use crate::server::SharedFileMap;
use crate::{
cfg::ConfigOptsServe,
server::{
@@ -16,7 +17,6 @@ use std::{
process::{Child, Command},
sync::{Arc, RwLock},
};
use tokio::sync::broadcast::{self};
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
@@ -33,7 +33,7 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
) -> Result<()> {
set_ctrl_c(&config);
let hot_reload_state = match config.hot_reload {
let file_map = match config.hot_reload {
true => {
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
@@ -44,16 +44,16 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
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(),
})
Some(file_map.clone())
}
false => None,
};
let hot_reload_state = HotReloadState {
receiver: Default::default(),
file_map,
};
serve::<P>(config, serve_cfg, hot_reload_state).await?;
Ok(())
@@ -73,15 +73,15 @@ fn set_ctrl_c(config: &CrateConfig) {
async fn serve<P: Platform + Send + 'static>(
config: CrateConfig,
serve: &ConfigOptsServe,
hot_reload_state: Option<HotReloadState>,
hot_reload_state: HotReloadState,
) -> Result<()> {
let hot_reload: tokio::task::JoinHandle<Result<()>> = tokio::spawn({
let hot_reload_state = hot_reload_state.clone();
async move {
match hot_reload_state {
Some(hot_reload_state) => {
match hot_reload_state.file_map.clone() {
Some(file_map) => {
// The open interprocess sockets
start_desktop_hot_reload(hot_reload_state).await?;
start_desktop_hot_reload(hot_reload_state, file_map).await?;
}
None => {
std::future::pending::<()>().await;
@@ -113,7 +113,10 @@ async fn serve<P: Platform + Send + 'static>(
Ok(())
}
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
async fn start_desktop_hot_reload(
hot_reload_state: HotReloadState,
file_map: SharedFileMap,
) -> Result<()> {
let metadata = cargo_metadata::MetadataCommand::new()
.no_deps()
.exec()
@@ -137,7 +140,6 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
// listen for connections
std::thread::spawn({
let file_map = hot_reload_state.file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
move || {
@@ -184,7 +186,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
}
});
let mut hot_reload_rx = hot_reload_state.messages.subscribe();
let mut hot_reload_rx = hot_reload_state.receiver.subscribe();
while let Ok(msg) = hot_reload_rx.recv().await {
let channels = &mut *channels.lock().unwrap();
+19 -25
View File
@@ -2,14 +2,12 @@ use crate::{cfg::ConfigOptsServe, BuildResult, Result};
use dioxus_cli_config::CrateConfig;
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
use dioxus_hot_reload::HotReloadMsg;
use dioxus_hot_reload::{HotReloadMsg, HotReloadReceiver};
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use fs_extra::dir::CopyOptions;
use notify::{RecommendedWatcher, Watcher};
use std::{path::PathBuf, sync::Arc};
use tokio::sync::broadcast::{self};
mod output;
use output::*;
@@ -19,25 +17,14 @@ pub mod web;
#[derive(Clone)]
pub struct HotReloadState {
/// Pending hotreload updates to be sent to all connected clients
pub messages: broadcast::Sender<HotReloadMsg>,
/// The receiver for hot reload messages
pub receiver: HotReloadReceiver,
/// The file map that tracks the state of the projecta
pub file_map: SharedFileMap,
pub file_map: Option<SharedFileMap>,
}
type SharedFileMap = Arc<Mutex<FileMap<HtmlCtx>>>;
impl HotReloadState {
pub fn all_templates(&self) -> Vec<Template> {
self.file_map
.lock()
.unwrap()
.map
.values()
.flat_map(|v| v.templates.values().copied())
.collect()
}
}
type SharedFileMap = Arc<Mutex<FileMap<HtmlCtx>>>;
/// Sets up a file watcher.
///
@@ -46,7 +33,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
build_with: F,
config: &CrateConfig,
web_info: Option<WebServerInfo>,
hot_reload: Option<HotReloadState>,
hot_reload: HotReloadState,
) -> Result<RecommendedWatcher> {
let mut last_update_time = chrono::Local::now().timestamp();
@@ -96,7 +83,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
fn watch_event<F>(
event: notify::Event,
last_update_time: &mut i64,
hot_reload: &Option<HotReloadState>,
hot_reload: &HotReloadState,
config: &CrateConfig,
build_with: &F,
web_info: &Option<WebServerInfo>,
@@ -119,8 +106,14 @@ fn watch_event<F>(
// By default we want to not do a full rebuild, and instead let the hot reload system invalidate it
let mut needs_full_rebuild = false;
if let Some(hot_reload) = &hot_reload {
hotreload_files(hot_reload, &mut needs_full_rebuild, &event, config);
if let Some(file_map) = &hot_reload.file_map {
hotreload_files(
hot_reload,
file_map,
&mut needs_full_rebuild,
&event,
config,
);
}
if needs_full_rebuild {
@@ -161,12 +154,13 @@ fn full_rebuild<F>(
fn hotreload_files(
hot_reload: &HotReloadState,
file_map: &SharedFileMap,
needs_full_rebuild: &mut bool,
event: &notify::Event,
config: &CrateConfig,
) {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut rsx_file_map = file_map.lock().unwrap();
let mut messages: Vec<HotReloadMsg> = Vec::new();
for path in &event.paths {
@@ -220,7 +214,7 @@ fn hotreload_files(
}
for msg in messages {
let _ = hot_reload.messages.send(msg);
hot_reload.receiver.send_message(msg);
}
}
@@ -298,7 +292,7 @@ fn attempt_css_reload(
_ = rsx_file_map.is_tracking_asset(&local_path)?;
// copy the asset over to the output directory
// todo this whole css hotreloading shouldbe less hacky and more robust
// todo this whole css hotreloading should be less hacky and more robust
_ = fs_extra::copy_items(
&[path],
config.out_dir(),
-82
View File
@@ -1,82 +0,0 @@
use crate::server::HotReloadState;
use axum::{
extract::{
ws::{Message, WebSocket},
WebSocketUpgrade,
},
response::IntoResponse,
Extension,
};
use dioxus_hot_reload::HotReloadMsg;
use futures_util::{pin_mut, FutureExt};
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<HotReloadState>,
) -> 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: HotReloadState) -> anyhow::Result<()> {
tracing::info!("🔥 Hot Reload WebSocket connected");
// 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_templates() {
socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await?;
}
let mut rx = state.messages.subscribe();
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(())
}
+20 -32
View File
@@ -4,7 +4,7 @@ use crate::{
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
setup_file_watcher, HotReloadState,
setup_file_watcher,
},
BuildResult, Result,
};
@@ -14,28 +14,20 @@ use std::{
net::{SocketAddr, UdpSocket},
sync::Arc,
};
use tokio::sync::broadcast;
mod hot_reload;
mod proxy;
mod server;
use server::*;
pub struct WsReloadState {
update: broadcast::Sender<()>,
}
use super::HotReloadState;
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 mut hot_reload_state = None;
if config.hot_reload {
hot_reload_state = Some(build_hotreload_filemap(&config));
}
let hot_reload_state = build_hotreload_filemap(&config);
serve(ip, config, hot_reload_state, serve_cfg).await
}
@@ -44,7 +36,7 @@ pub async fn startup(config: CrateConfig, serve_cfg: &ConfigOptsServe) -> Result
pub async fn serve(
ip: String,
config: CrateConfig,
hot_reload_state: Option<HotReloadState>,
hot_reload_state: HotReloadState,
opts: &ConfigOptsServe,
) -> Result<()> {
let skip_assets = opts.skip_assets;
@@ -59,16 +51,13 @@ pub async fn serve(
tracing::info!("🚀 Starting development server...");
// WS Reload Watching
let (reload_tx, _) = broadcast::channel(100);
// We got to own watcher so that it exists for the duration of serve
// Otherwise full reload won't work.
let _watcher = setup_file_watcher(
{
let config = config.clone();
let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx, skip_assets)
let hot_reload_state = hot_reload_state.clone();
move || build(&config, &hot_reload_state, skip_assets)
},
&config,
Some(WebServerInfo {
@@ -79,10 +68,6 @@ pub async fn serve(
)
.await?;
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// HTTPS
// Before console info so it can stop if mkcert isn't installed or fails
let rustls_config = get_rustls(&config).await?;
@@ -102,7 +87,7 @@ pub async fn serve(
);
// Router
let router = setup_router(config.clone(), ws_reload_state, hot_reload_state).await?;
let router = setup_router(config.clone(), hot_reload_state).await?;
// Start server
start_server(port, router, opts.open, rustls_config, &config).await?;
@@ -173,7 +158,7 @@ fn get_ip() -> Option<String> {
fn build(
config: &CrateConfig,
reload_tx: &broadcast::Sender<()>,
hot_reload_state: &HotReloadState,
skip_assets: bool,
) -> Result<BuildResult> {
// Since web platform doesn't use `rust_flags`, this argument is explicitly
@@ -189,7 +174,7 @@ fn build(
}
}
let _ = reload_tx.send(());
hot_reload_state.receiver.reload();
result
}
@@ -207,14 +192,17 @@ fn set_ctrlc_handler(config: &CrateConfig) {
}
fn build_hotreload_filemap(config: &CrateConfig) -> HotReloadState {
let FileMapBuildResult { map, errors } = FileMap::create(config.crate_dir.clone()).unwrap();
for err in errors {
tracing::error!("{}", err);
}
HotReloadState {
messages: broadcast::channel(100).0.clone(),
file_map: Arc::new(Mutex::new(map)).clone(),
file_map: config.hot_reload.then(|| {
let FileMapBuildResult { map, errors } =
FileMap::create(config.crate_dir.clone()).unwrap();
for err in errors {
tracing::error!("{}", err);
}
Arc::new(Mutex::new(map))
}),
receiver: Default::default(),
}
}
+6 -46
View File
@@ -1,11 +1,7 @@
use super::{hot_reload::*, WsReloadState};
use crate::{server::HotReloadState, Result};
use axum::{
body::Body,
extract::{
ws::{Message, WebSocket},
Extension, WebSocketUpgrade,
},
extract::Extension,
http::{
self,
header::{HeaderName, HeaderValue},
@@ -17,7 +13,8 @@ use axum::{
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::{CrateConfig, WebHttpsConfig};
use std::{fs, io, process::Command, sync::Arc};
use dioxus_hot_reload::HotReloadRouterExt;
use std::{fs, io, process::Command};
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
@@ -26,11 +23,7 @@ use tower_http::{
};
/// Sets up and returns a router
pub async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<HotReloadState>,
) -> Result<Router> {
pub async fn setup_router(config: CrateConfig, hot_reload: HotReloadState) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
@@ -93,17 +86,12 @@ pub async fn setup_router(
};
// Setup websocket
router = router.route("/_dioxus/ws", get(ws_handler));
router = router.connect_hot_reload();
// 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))
}
.layer(Extension(hot_reload.receiver.clone()));
Ok(router)
}
@@ -138,34 +126,6 @@ fn no_cache(
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";