mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 12:13:04 +00:00
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:
parent
460b70e0f0
commit
245003a5d4
59 changed files with 1507 additions and 791 deletions
65
Cargo.lock
generated
65
Cargo.lock
generated
|
@ -2096,6 +2096,7 @@ dependencies = [
|
|||
"dioxus-router",
|
||||
"dioxus-signals",
|
||||
"dioxus-ssr",
|
||||
"dioxus-static-site-generation",
|
||||
"dioxus-web",
|
||||
"env_logger 0.10.2",
|
||||
"futures-util",
|
||||
|
@ -2415,17 +2416,22 @@ dependencies = [
|
|||
name = "dioxus-hot-reload"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"dioxus-core 0.5.2",
|
||||
"dioxus-html",
|
||||
"dioxus-rsx",
|
||||
"execute",
|
||||
"futures-util",
|
||||
"ignore",
|
||||
"interprocess-docfix",
|
||||
"notify",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2583,6 +2589,7 @@ dependencies = [
|
|||
"dioxus-ssr",
|
||||
"gloo",
|
||||
"gloo-utils 0.1.7",
|
||||
"http 1.1.0",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -2650,6 +2657,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"chrono",
|
||||
"dioxus",
|
||||
"dioxus-cli-config",
|
||||
"dioxus-core 0.5.2",
|
||||
"dioxus-html",
|
||||
"dioxus-signals",
|
||||
|
@ -2666,6 +2674,26 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-static-site-generation"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"dioxus",
|
||||
"dioxus-cli-config",
|
||||
"dioxus-fullstack",
|
||||
"dioxus-hot-reload",
|
||||
"dioxus-lib",
|
||||
"dioxus-router",
|
||||
"dioxus-ssr",
|
||||
"dioxus-web",
|
||||
"http 1.1.0",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dioxus-tailwind"
|
||||
version = "0.0.0"
|
||||
|
@ -3668,6 +3696,15 @@ dependencies = [
|
|||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "github-pages-static-generation"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dioxus",
|
||||
"tower",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-actor"
|
||||
version = "0.31.1"
|
||||
|
@ -7585,6 +7622,14 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "router-static-generation"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dioxus",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.6"
|
||||
|
@ -8222,6 +8267,14 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640"
|
||||
|
||||
[[package]]
|
||||
name = "simple-static-generation"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dioxus",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_logger"
|
||||
version = "4.3.3"
|
||||
|
@ -8606,18 +8659,6 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "static-hydrated"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dioxus",
|
||||
"dioxus-fullstack",
|
||||
"dioxus-router",
|
||||
"dioxus-web",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -29,12 +29,15 @@ members = [
|
|||
"packages/hot-reload",
|
||||
"packages/fullstack",
|
||||
"packages/server-macro",
|
||||
"packages/static-generation",
|
||||
"packages/fullstack/examples/axum-hello-world",
|
||||
"packages/fullstack/examples/axum-router",
|
||||
"packages/fullstack/examples/axum-streaming",
|
||||
"packages/fullstack/examples/axum-desktop",
|
||||
"packages/fullstack/examples/axum-auth",
|
||||
"packages/fullstack/examples/static-hydrated",
|
||||
"packages/static-generation/examples/simple",
|
||||
"packages/static-generation/examples/router",
|
||||
"packages/static-generation/examples/github-pages",
|
||||
# Full project examples
|
||||
"examples/tailwind",
|
||||
"examples/PWA-example",
|
||||
|
@ -76,8 +79,8 @@ dioxus-cli-config = { path = "packages/cli-config", version = "0.5.0", default-f
|
|||
generational-box = { path = "packages/generational-box", version = "0.5.0" }
|
||||
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.5.0" }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.5.0" }
|
||||
dioxus-static-site-generation = { path = "packages/static-generation", version = "0.5.0" }
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.5.0", default-features = false }
|
||||
dioxus-ext = { path = "packages/extension" }
|
||||
tracing = "0.1.37"
|
||||
tracing-futures = "0.2.5"
|
||||
toml = "0.8"
|
||||
|
@ -101,7 +104,7 @@ axum = "0.7.0"
|
|||
axum-server = { version = "0.6.0", default-features = false }
|
||||
tower = "0.4.13"
|
||||
http = "1.0.0"
|
||||
tower-http = "0.5.1"
|
||||
tower-http = "0.5.2"
|
||||
hyper = "1.0.0"
|
||||
hyper-rustls = "0.26.0"
|
||||
serde_json = "1.0.61"
|
||||
|
@ -122,9 +125,6 @@ isnta = "1.36.1"
|
|||
[profile.dev.package.insta]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.similar]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.dioxus-core-macro]
|
||||
opt-level = 3
|
||||
|
||||
|
|
|
@ -22,6 +22,11 @@ pub enum Platform {
|
|||
#[cfg_attr(feature = "cli", clap(name = "fullstack"))]
|
||||
#[serde(rename = "fullstack")]
|
||||
Fullstack,
|
||||
|
||||
/// Targeting the static generation platform using SSR and Dioxus-Fullstack
|
||||
#[cfg_attr(feature = "cli", clap(name = "fullstack"))]
|
||||
#[serde(rename = "static-generation")]
|
||||
StaticGeneration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
@ -93,7 +93,7 @@ rsx-rosetta = { workspace = true }
|
|||
dioxus-rsx = { workspace = true }
|
||||
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-hot-reload = { workspace = true }
|
||||
dioxus-hot-reload = { workspace = true, features = ["serve"] }
|
||||
interprocess = { workspace = true }
|
||||
# interprocess-docfix = { version = "1.2.2" }
|
||||
ignore = "0.4.22"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: ¬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 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(),
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ quote = { workspace = true }
|
|||
[features]
|
||||
default = []
|
||||
fullstack = []
|
||||
static-generation = []
|
||||
desktop = []
|
||||
mobile = []
|
||||
web = []
|
||||
|
|
|
@ -83,7 +83,22 @@ pub fn mobile(input: TokenStream) -> TokenStream {
|
|||
|
||||
#[proc_macro]
|
||||
pub fn fullstack(input: TokenStream) -> TokenStream {
|
||||
if cfg!(feature = "web") {
|
||||
if cfg!(feature = "fullstack") {
|
||||
let input = TokenStream2::from(input);
|
||||
quote! {
|
||||
#input
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
{}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn static_generation(input: TokenStream) -> TokenStream {
|
||||
if cfg!(feature = "static-generation") {
|
||||
let input = TokenStream2::from(input);
|
||||
quote! {
|
||||
#input
|
||||
|
|
|
@ -22,6 +22,7 @@ dioxus-web = { workspace = true, optional = true }
|
|||
dioxus-mobile = { workspace = true, optional = true }
|
||||
dioxus-desktop = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-fullstack = { workspace = true, optional = true }
|
||||
dioxus-static-site-generation = { workspace = true, optional = true }
|
||||
dioxus-liveview = { workspace = true, optional = true }
|
||||
dioxus-ssr ={ workspace = true, optional = true }
|
||||
|
||||
|
@ -45,10 +46,11 @@ router = ["dioxus-router"]
|
|||
fullstack = ["dioxus-fullstack", "dioxus-config-macro/fullstack", "serde", "dioxus-router?/fullstack"]
|
||||
desktop = ["dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"]
|
||||
mobile = ["dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
|
||||
web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "dioxus-router?/web"]
|
||||
web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation?/web", "dioxus-config-macro/web", "dioxus-router?/web"]
|
||||
ssr = ["dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
|
||||
liveview = ["dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
|
||||
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"]
|
||||
static-generation = ["dioxus-static-site-generation", "dioxus-config-macro/static-generation"]
|
||||
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum"]
|
||||
|
||||
# This feature just disables the no-renderer-enabled warning
|
||||
third-party-renderer = []
|
||||
|
|
|
@ -17,10 +17,18 @@ pub struct LaunchBuilder<Cfg: 'static = (), ContextFn: ?Sized = ValidContext> {
|
|||
|
||||
pub type LaunchFn<Cfg, Context> = fn(fn() -> Element, Vec<Box<Context>>, Cfg);
|
||||
|
||||
#[cfg(any(feature = "fullstack", feature = "liveview"))]
|
||||
#[cfg(any(
|
||||
feature = "fullstack",
|
||||
feature = "static-generation",
|
||||
feature = "liveview"
|
||||
))]
|
||||
type ValidContext = SendContext;
|
||||
|
||||
#[cfg(not(any(feature = "fullstack", feature = "liveview")))]
|
||||
#[cfg(not(any(
|
||||
feature = "fullstack",
|
||||
feature = "static-generation",
|
||||
feature = "liveview"
|
||||
)))]
|
||||
type ValidContext = UnsendContext;
|
||||
|
||||
type SendContext = dyn Fn() -> Box<dyn Any> + Send + Sync + 'static;
|
||||
|
@ -178,6 +186,7 @@ impl<Cfg: Default + 'static, ContextFn: ?Sized> LaunchBuilder<Cfg, ContextFn> {
|
|||
/// - `fullstack`
|
||||
/// - `desktop`
|
||||
/// - `mobile`
|
||||
/// - `static-generation`
|
||||
/// - `web`
|
||||
/// - `liveview`
|
||||
mod current_platform {
|
||||
|
@ -195,6 +204,9 @@ mod current_platform {
|
|||
}
|
||||
use crate::prelude::TryIntoConfig;
|
||||
|
||||
#[cfg(feature = "fullstack")]
|
||||
pub use dioxus_fullstack::launch::*;
|
||||
|
||||
#[cfg(any(feature = "desktop", feature = "mobile"))]
|
||||
if_else_cfg! {
|
||||
if not(feature = "fullstack") {
|
||||
|
@ -211,12 +223,22 @@ mod current_platform {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fullstack")]
|
||||
pub use dioxus_fullstack::launch::*;
|
||||
#[cfg(feature = "static-generation")]
|
||||
if_else_cfg! {
|
||||
if all(not(feature = "fullstack"), not(feature = "desktop"), not(feature = "mobile")) {
|
||||
pub use dioxus_static_site_generation::launch::*;
|
||||
} else {
|
||||
impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_static_site_generation::Config {
|
||||
fn into_config(self) -> Option<crate::launch::current_platform::Config> {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
if_else_cfg! {
|
||||
if not(any(feature = "desktop", feature = "mobile", feature = "fullstack")) {
|
||||
if not(any(feature = "desktop", feature = "mobile", feature = "fullstack", feature = "static-generation")) {
|
||||
pub use dioxus_web::launch::*;
|
||||
} else {
|
||||
impl TryIntoConfig<crate::launch::current_platform::Config> for ::dioxus_web::Config {
|
||||
|
@ -234,7 +256,8 @@ mod current_platform {
|
|||
feature = "web",
|
||||
feature = "desktop",
|
||||
feature = "mobile",
|
||||
feature = "fullstack"
|
||||
feature = "fullstack",
|
||||
feature = "static-generation"
|
||||
))
|
||||
{
|
||||
pub use dioxus_liveview::launch::*;
|
||||
|
@ -252,7 +275,8 @@ mod current_platform {
|
|||
feature = "desktop",
|
||||
feature = "mobile",
|
||||
feature = "web",
|
||||
feature = "fullstack"
|
||||
feature = "fullstack",
|
||||
feature = "static-generation"
|
||||
)))]
|
||||
pub type Config = ();
|
||||
|
||||
|
@ -261,7 +285,8 @@ mod current_platform {
|
|||
feature = "desktop",
|
||||
feature = "mobile",
|
||||
feature = "web",
|
||||
feature = "fullstack"
|
||||
feature = "fullstack",
|
||||
feature = "static-generation"
|
||||
)))]
|
||||
pub fn launch(
|
||||
root: fn() -> dioxus_core::Element,
|
||||
|
|
|
@ -101,6 +101,10 @@ pub use dioxus_router as router;
|
|||
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
|
||||
pub use dioxus_fullstack as fullstack;
|
||||
|
||||
#[cfg(feature = "static-generation")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "static-generation")))]
|
||||
pub use dioxus_static_site_generation as static_site_generation;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
|
||||
pub use dioxus_desktop as desktop;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-fullstack"
|
||||
authors = ["Jonathan Kelley, Evan Almloff"]
|
||||
authors = ["Jonathan Kelley", "Evan Almloff"]
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
description = "Fullstack Dioxus Utilities"
|
||||
|
@ -62,7 +62,7 @@ dioxus-cli-config = { workspace = true, features = ["read-config"], optional = t
|
|||
tokio = { workspace = true, features = ["rt", "sync"], optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
dioxus-hot-reload = { workspace = true }
|
||||
dioxus-hot-reload = { workspace = true, features = ["serve"] }
|
||||
tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -70,13 +70,14 @@ dioxus = { workspace = true, features = ["fullstack"] }
|
|||
|
||||
[features]
|
||||
default = ["hot-reload"]
|
||||
hot-reload = ["serde_json"]
|
||||
hot-reload = ["serde_json", "dioxus-hot-reload/serve"]
|
||||
web = ["dioxus-web", "web-sys"]
|
||||
desktop = ["dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
|
||||
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"]
|
||||
static-site-generation = []
|
||||
server = [
|
||||
"server_fn/ssr",
|
||||
"dioxus_server_macro/server",
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
[package]
|
||||
name = "static-hydrated"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-router = { workspace = true, features = ["fullstack"] }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = "1.0.159"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["tokio", "dioxus-fullstack/server"]
|
||||
web = ["dioxus-web"]
|
|
@ -1,46 +0,0 @@
|
|||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "Dioxus"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# desktop, web, mobile, ssr
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "docs"
|
||||
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = ["tailwind.css"]
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# serve: [dev-server] only
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
|
@ -1,90 +0,0 @@
|
|||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx build --features web --release
|
||||
//! cargo run --features server
|
||||
//! ```
|
||||
|
||||
#![allow(unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::{launch, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Generate all routes and output them to the docs path
|
||||
#[cfg(feature = "server")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let wrapper = DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
</head>
|
||||
<body>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
};
|
||||
let mut renderer = IncrementalRenderer::builder().build();
|
||||
pre_cache_static_routes::<Route, _>(&mut renderer, &wrapper)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Hydrate the page
|
||||
#[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()
|
||||
.expect("Failed to get root props from document"),
|
||||
dioxus_web::Config::default().hydrate(true),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, Serialize, Deserialize)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog() -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog {}, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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())?;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -23,7 +23,18 @@ execute = { version = "0.2.11", optional = true }
|
|||
once_cell = { version = "1.17.0", optional = true }
|
||||
ignore = { version = "0.4.19", optional = true }
|
||||
|
||||
# hot reloading serve
|
||||
axum = { workspace = true, features = ["ws"], optional = true }
|
||||
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
|
||||
futures-util = { workspace = true, features = ["async-await-macro"], optional = true }
|
||||
tokio = { workspace = true, features = ["sync"], optional = true }
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
[features]
|
||||
default = ["dioxus-html"]
|
||||
custom_file_watcher = ["ignore", "chrono", "notify", "execute", "once_cell", "ignore"]
|
||||
file_watcher = ["custom_file_watcher", "dioxus-html/hot-reload-context"]
|
||||
serve = ["axum", "tokio-stream", "futures-util", "tokio", "file_watcher"]
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
// 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";
|
|
@ -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
packages/hot-reload/src/websocket.rs
Normal file
279
packages/hot-reload/src/websocket.rs
Normal 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
|
||||
}
|
|
@ -26,13 +26,14 @@ js-sys = { version = "0.3.63", optional = true }
|
|||
gloo-utils = { version = "0.1.6", optional = true }
|
||||
dioxus-liveview = { workspace = true, optional = true }
|
||||
dioxus-ssr = { workspace = true, optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
dioxus-fullstack = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
dioxus-cli-config = { workspace = true, features = ["read-config"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ssr = ["dioxus-ssr/incremental", "tokio", "dioxus-fullstack?/server"]
|
||||
ssr = ["dioxus-ssr/incremental", "tokio", "dioxus-fullstack?/server", "http"]
|
||||
liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"]
|
||||
wasm_test = []
|
||||
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys", "dioxus-router-macro/web"]
|
||||
|
@ -41,7 +42,6 @@ fullstack = ["dioxus-fullstack"]
|
|||
[dev-dependencies]
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
dioxus = { workspace = true, features = ["router"] }
|
||||
# dioxus-liveview = { workspace = true, features = ["axum"] }
|
||||
dioxus-ssr = { workspace = true }
|
||||
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
|
||||
|
||||
|
|
|
@ -2,7 +2,11 @@ use dioxus_lib::prelude::*;
|
|||
|
||||
use std::{cell::RefCell, rc::Rc, str::FromStr};
|
||||
|
||||
use crate::{prelude::Outlet, routable::Routable, router_cfg::RouterConfig};
|
||||
use crate::{
|
||||
prelude::{provide_router_context, Outlet},
|
||||
routable::Routable,
|
||||
router_cfg::RouterConfig,
|
||||
};
|
||||
|
||||
/// The config for [`Router`].
|
||||
#[derive(Clone)]
|
||||
|
@ -78,7 +82,7 @@ where
|
|||
use crate::prelude::{outlet::OutletContext, RouterContext};
|
||||
|
||||
use_hook(|| {
|
||||
provide_context(RouterContext::new(
|
||||
provide_router_context(RouterContext::new(
|
||||
(props
|
||||
.config
|
||||
.config
|
||||
|
|
|
@ -9,11 +9,37 @@ use dioxus_lib::prelude::*;
|
|||
|
||||
use crate::{
|
||||
navigation::NavigationTarget,
|
||||
prelude::{AnyHistoryProvider, IntoRoutable},
|
||||
prelude::{AnyHistoryProvider, IntoRoutable, SiteMapSegment},
|
||||
routable::Routable,
|
||||
router_cfg::RouterConfig,
|
||||
};
|
||||
|
||||
/// This context is set in the root of the virtual dom if there is a router present.
|
||||
#[derive(Clone, Copy)]
|
||||
struct RootRouterContext(Signal<Option<RouterContext>>);
|
||||
|
||||
/// Try to get the router that was created closest to the root of the virtual dom. This may be called outside of the router.
|
||||
///
|
||||
/// This will return `None` if there is no router present or the router has not been created yet.
|
||||
pub fn root_router() -> Option<RouterContext> {
|
||||
if let Some(ctx) = ScopeId::ROOT.consume_context::<RootRouterContext>() {
|
||||
ctx.0.cloned()
|
||||
} else {
|
||||
ScopeId::ROOT.provide_context(RootRouterContext(Signal::new_in_scope(None, ScopeId::ROOT)));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn provide_router_context(ctx: RouterContext) {
|
||||
if root_router().is_none() {
|
||||
ScopeId::ROOT.provide_context(RootRouterContext(Signal::new_in_scope(
|
||||
Some(ctx),
|
||||
ScopeId::ROOT,
|
||||
)));
|
||||
}
|
||||
provide_context(ctx);
|
||||
}
|
||||
|
||||
/// An error that can occur when navigating.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExternalNavigationFailure(pub String);
|
||||
|
@ -39,6 +65,8 @@ struct RouterContextInner {
|
|||
failure_external_navigation: fn() -> Element,
|
||||
|
||||
any_route_to_string: fn(&dyn Any) -> String,
|
||||
|
||||
site_map: &'static [SiteMapSegment],
|
||||
}
|
||||
|
||||
impl RouterContextInner {
|
||||
|
@ -119,6 +147,8 @@ impl RouterContext {
|
|||
})
|
||||
.to_string()
|
||||
},
|
||||
|
||||
site_map: R::SITE_MAP,
|
||||
};
|
||||
|
||||
// set the updater
|
||||
|
@ -281,6 +311,11 @@ impl RouterContext {
|
|||
write_inner.update_subscribers();
|
||||
}
|
||||
|
||||
/// Get the site map of the router.
|
||||
pub fn site_map(&self) -> &'static [SiteMapSegment] {
|
||||
self.inner.read().site_map
|
||||
}
|
||||
|
||||
pub(crate) fn render_error(&self) -> Element {
|
||||
let inner_read = self.inner.write_unchecked();
|
||||
inner_read
|
||||
|
|
|
@ -3,12 +3,8 @@ use core::pin::Pin;
|
|||
use std::future::Future;
|
||||
use std::str::FromStr;
|
||||
|
||||
use dioxus_lib::prelude::*;
|
||||
use dioxus_ssr::incremental::{
|
||||
IncrementalRenderer, IncrementalRendererError, RenderFreshness, WrapBody,
|
||||
};
|
||||
|
||||
use crate::prelude::*;
|
||||
use dioxus_lib::prelude::*;
|
||||
|
||||
/// Pre-cache all static routes.
|
||||
pub async fn pre_cache_static_routes<Rt, R: WrapBody + Send + Sync>(
|
||||
|
@ -19,51 +15,20 @@ where
|
|||
Rt: Routable,
|
||||
<Rt as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
for route in Rt::SITE_MAP
|
||||
.iter()
|
||||
.flat_map(|seg| seg.flatten().into_iter())
|
||||
{
|
||||
// check if this is a static segment
|
||||
let mut is_static = true;
|
||||
let mut full_path = String::new();
|
||||
for segment in &route {
|
||||
match segment {
|
||||
SegmentType::Child => {}
|
||||
SegmentType::Static(s) => {
|
||||
full_path += "/";
|
||||
full_path += s;
|
||||
}
|
||||
_ => {
|
||||
// skip routes with any dynamic segments
|
||||
is_static = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is_static {
|
||||
match Rt::from_str(&full_path) {
|
||||
Ok(route) => {
|
||||
render_route(
|
||||
renderer,
|
||||
route,
|
||||
&mut tokio::io::sink(),
|
||||
|vdom| {
|
||||
Box::pin(async move {
|
||||
vdom.rebuild_in_place();
|
||||
vdom.wait_for_suspense().await;
|
||||
})
|
||||
},
|
||||
wrapper,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::info!("@ route: {}", full_path);
|
||||
tracing::error!("Error pre-caching static route: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
for route in Rt::static_routes() {
|
||||
render_route(
|
||||
renderer,
|
||||
route,
|
||||
&mut tokio::io::sink(),
|
||||
|vdom| {
|
||||
Box::pin(async move {
|
||||
vdom.rebuild_in_place();
|
||||
vdom.wait_for_suspense().await;
|
||||
})
|
||||
},
|
||||
wrapper,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -35,6 +35,7 @@ mod contexts {
|
|||
pub(crate) mod router;
|
||||
pub use navigator::*;
|
||||
pub(crate) use router::*;
|
||||
pub use router::{root_router, RouterContext};
|
||||
}
|
||||
|
||||
mod router_cfg;
|
||||
|
|
|
@ -233,23 +233,6 @@ type SiteMapFlattened<'a> = FlatMap<
|
|||
fn(&SiteMapSegment) -> Vec<Vec<SegmentType>>,
|
||||
>;
|
||||
|
||||
fn seg_strs_to_route<T>(segs_maybe: &Option<Vec<&str>>) -> Option<T>
|
||||
where
|
||||
T: Routable,
|
||||
{
|
||||
if let Some(str) = seg_strs_to_str(segs_maybe) {
|
||||
T::from_str(&str).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn seg_strs_to_str(segs_maybe: &Option<Vec<&str>>) -> Option<String> {
|
||||
segs_maybe
|
||||
.as_ref()
|
||||
.map(|segs| String::from('/') + &segs.join("/"))
|
||||
}
|
||||
|
||||
/// Something that can be:
|
||||
/// 1. Converted from a route.
|
||||
/// 2. Converted to a route.
|
||||
|
@ -354,16 +337,20 @@ pub trait Routable: FromStr + Display + Clone + 'static {
|
|||
/// Example static route: `#[route("/static/route")]`
|
||||
fn static_routes() -> Vec<Self> {
|
||||
Self::flatten_site_map()
|
||||
.filter_map(|route| {
|
||||
let route_if_static = &route
|
||||
.iter()
|
||||
.map(|segment| match segment {
|
||||
SegmentType::Static(s) => Some(*s),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<_>>>();
|
||||
.filter_map(|segments| {
|
||||
let mut route = String::new();
|
||||
for segment in segments.iter() {
|
||||
match segment {
|
||||
SegmentType::Static(s) => {
|
||||
route.push('/');
|
||||
route.push_str(s)
|
||||
}
|
||||
SegmentType::Child => {}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
seg_strs_to_route(route_if_static)
|
||||
route.parse().ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -413,6 +400,40 @@ pub enum SegmentType {
|
|||
Child,
|
||||
}
|
||||
|
||||
impl SegmentType {
|
||||
/// Try to convert this segment into a static segment.
|
||||
pub fn to_static(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
SegmentType::Static(s) => Some(*s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert this segment into a dynamic segment.
|
||||
pub fn to_dynamic(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
SegmentType::Dynamic(s) => Some(*s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert this segment into a catch all segment.
|
||||
pub fn to_catch_all(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
SegmentType::CatchAll(s) => Some(*s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert this segment into a child segment.
|
||||
pub fn to_child(&self) -> Option<()> {
|
||||
match self {
|
||||
SegmentType::Child => Some(()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SegmentType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
|
|
53
packages/router/tests/site_map.rs
Normal file
53
packages/router/tests/site_map.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn with_class() {
|
||||
#[derive(Routable, Clone, PartialEq, Debug)]
|
||||
enum ChildRoute {
|
||||
#[route("/")]
|
||||
ChildRoot {},
|
||||
#[route("/:not_static")]
|
||||
NotStatic { not_static: String },
|
||||
}
|
||||
|
||||
#[derive(Routable, Clone, PartialEq, Debug)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Root {},
|
||||
#[route("/test")]
|
||||
Test {},
|
||||
#[child("/child")]
|
||||
Nested { child: ChildRoute },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Test() -> Element {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Root() -> Element {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ChildRoot() -> Element {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotStatic(not_static: String) -> Element {
|
||||
todo!()
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Route::static_routes(),
|
||||
vec![
|
||||
Route::Root {},
|
||||
Route::Test {},
|
||||
Route::Nested {
|
||||
child: ChildRoute::ChildRoot {}
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
|
@ -11,6 +11,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"]
|
|||
[dependencies]
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, features = ["eval"]}
|
||||
dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true }
|
||||
generational-box = { workspace = true }
|
||||
askama_escape = "0.10.3"
|
||||
thiserror = "1.0.23"
|
||||
|
@ -42,4 +43,4 @@ fs_extra = "1.2.0"
|
|||
|
||||
[features]
|
||||
default = []
|
||||
incremental = ["dep:tokio", "chrono"]
|
||||
incremental = ["dep:tokio", "chrono", "dioxus-cli-config"]
|
||||
|
|
|
@ -29,14 +29,21 @@ pub struct DefaultRenderer {
|
|||
|
||||
impl Default for DefaultRenderer {
|
||||
fn default() -> Self {
|
||||
let before = r#"<!DOCTYPE html>
|
||||
let title = dioxus_cli_config::CURRENT_CONFIG
|
||||
.as_ref()
|
||||
.map(|c| c.dioxus_config.application.name.clone())
|
||||
.unwrap_or("Dioxus Application".into());
|
||||
let before = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
<title>{}</title>
|
||||
</head>
|
||||
<body>"#;
|
||||
<body>"#,
|
||||
title
|
||||
);
|
||||
let after = r#"</body>
|
||||
</html>"#;
|
||||
Self {
|
||||
|
@ -68,6 +75,7 @@ pub struct IncrementalRendererConfig {
|
|||
invalidate_after: Option<Duration>,
|
||||
map_path: Option<PathMapFn>,
|
||||
clear_cache: bool,
|
||||
pre_render: bool,
|
||||
}
|
||||
|
||||
impl Default for IncrementalRendererConfig {
|
||||
|
@ -85,6 +93,7 @@ impl IncrementalRendererConfig {
|
|||
invalidate_after: None,
|
||||
map_path: None,
|
||||
clear_cache: true,
|
||||
pre_render: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,19 +128,30 @@ impl IncrementalRendererConfig {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set whether to include hydration ids in the pre-rendered html.
|
||||
pub fn pre_render(mut self, pre_render: bool) -> Self {
|
||||
self.pre_render = pre_render;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the incremental renderer.
|
||||
pub fn build(self) -> IncrementalRenderer {
|
||||
let static_dir = self.static_dir.clone();
|
||||
let mut ssr_renderer = crate::Renderer::new();
|
||||
if self.pre_render {
|
||||
ssr_renderer.pre_render = true;
|
||||
}
|
||||
let mut renderer = IncrementalRenderer {
|
||||
static_dir: self.static_dir.clone(),
|
||||
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
|
||||
.map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
|
||||
invalidate_after: self.invalidate_after,
|
||||
ssr_renderer: crate::Renderer::new(),
|
||||
ssr_renderer,
|
||||
map_path: self.map_path.unwrap_or_else(move || {
|
||||
Arc::new(move |route: &str| {
|
||||
let (before_query, _) = route.split_once('?').unwrap_or((route, ""));
|
||||
let mut path = static_dir.clone();
|
||||
for segment in route.split('/') {
|
||||
for segment in before_query.split('/') {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
|
|
1
packages/static-generation/.gitignore
vendored
Normal file
1
packages/static-generation/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
target
|
34
packages/static-generation/Cargo.toml
Normal file
34
packages/static-generation/Cargo.toml
Normal file
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "dioxus-static-site-generation"
|
||||
authors = ["Evan Almloff"]
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
description = "Static site generation for Dioxus"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["ui", "gui", "react", "ssg"]
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-lib.workspace = true
|
||||
dioxus-router = { workspace = true, features = ["fullstack"]}
|
||||
dioxus-ssr = { workspace = true, features = ["incremental"], optional = true }
|
||||
axum = { workspace = true, features = ["ws", "macros"], optional = true }
|
||||
tower-http = { workspace = true, features = ["fs"], optional = true }
|
||||
dioxus-hot-reload = { workspace = true, features = ["serve"], optional = true }
|
||||
dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true }
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
tower = { workspace = true, features = ["util"], optional = true }
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus-fullstack/server", "dioxus-router/ssr", "dioxus-ssr", "tokio", "http", "dep:axum", "tower-http", "dioxus-hot-reload", "dioxus-cli-config", "tower"]
|
||||
web = ["dioxus-fullstack/web", "dioxus-router/web", "dioxus-web"]
|
83
packages/static-generation/README.md
Normal file
83
packages/static-generation/README.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
# Dioxus Fullstack
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-fullstack.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-fullstack
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/learn/0.5/) |
|
||||
[API Docs](https://docs.rs/dioxus-fullstack/latest/dioxus_sever) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
Fullstack utilities for the [`Dioxus`](https://dioxuslabs.com) framework.
|
||||
|
||||
# Features
|
||||
|
||||
- Integrates with the [Axum](https::/docs.rs/dioxus-fullstack/latest/dixous_server/axum_adapter/index.html) server framework with utilities for serving and rendering Dioxus applications.
|
||||
- [Server functions](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/attr.server.html) allow you to call code on the server from the client as if it were a normal function.
|
||||
- Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload).
|
||||
- Passing root props from the server to the client.
|
||||
|
||||
# Example
|
||||
|
||||
Full stack Dioxus in under 30 lines of code
|
||||
|
||||
```rust
|
||||
#![allow(non_snake_case)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
launch(App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let meaning = use_signal(|| None);
|
||||
|
||||
rsx! {
|
||||
h1 { "Meaning of life: {meaning:?}" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
|
||||
meaning.set(data);
|
||||
}
|
||||
},
|
||||
"Run a server function"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn get_meaning(of: String) -> Result<Option<u32>, ServerFnError> {
|
||||
Ok(of.contains("life").then(|| 42))
|
||||
}
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with full stack Dioxus, check out our [getting started guide](https://dioxuslabs.com/learn/0.5/getting_started), or the [full stack examples](https://github.com/DioxusLabs/dioxus/tree/master/packages/fullstack/examples).
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
4
packages/static-generation/examples/github-pages/.gitignore
vendored
Normal file
4
packages/static-generation/examples/github-pages/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
dist
|
||||
target
|
||||
docs
|
||||
.dioxus
|
17
packages/static-generation/examples/github-pages/Cargo.toml
Normal file
17
packages/static-generation/examples/github-pages/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "github-pages-static-generation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["static-generation", "router"] }
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
70
packages/static-generation/examples/github-pages/src/main.rs
Normal file
70
packages/static-generation/examples/github-pages/src/main.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
//! You can use the `github_pages` method to set up a preset for github pages.
|
||||
//! This will output your files in the `/docs` directory and set up a `404.html` file.
|
||||
|
||||
#![allow(unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// Generate all routes and output them to the static path
|
||||
fn main() {
|
||||
LaunchBuilder::new()
|
||||
.with_cfg(dioxus::static_site_generation::Config::new().github_pages())
|
||||
.launch(|| {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
|
||||
// You must include a catch all route to handle 404s
|
||||
#[route("/:..route")]
|
||||
PageNotFound { route: Vec<String> },
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog() -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello world!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog {}, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PageNotFound(route: Vec<String>) -> Element {
|
||||
rsx! {
|
||||
h1 { "Page not found" }
|
||||
p { "We are terribly sorry, but the page you requested doesn't exist." }
|
||||
pre { color: "red", "log:\nattempted to navigate to: {route:?}" }
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
docs
|
||||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
16
packages/static-generation/examples/router/Cargo.toml
Normal file
16
packages/static-generation/examples/router/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "router-static-generation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["static-generation", "router"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
54
packages/static-generation/examples/router/src/main.rs
Normal file
54
packages/static-generation/examples/router/src/main.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
//! Static generation works out of the box with the router. Just add a router anywhere in your app and it will generate any static routes for you!
|
||||
|
||||
#![allow(unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// Generate all routes and output them to the static path
|
||||
fn main() {
|
||||
launch(|| {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Blog() -> Element {
|
||||
rsx! {
|
||||
Link { to: Route::Home {}, "Go to counter" }
|
||||
table {
|
||||
tbody {
|
||||
for _ in 0..100 {
|
||||
tr {
|
||||
for _ in 0..100 {
|
||||
td { "hello!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
Link { to: Route::Blog {}, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
||||
}
|
4
packages/static-generation/examples/simple/.gitignore
vendored
Normal file
4
packages/static-generation/examples/simple/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
16
packages/static-generation/examples/simple/Cargo.toml
Normal file
16
packages/static-generation/examples/simple/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "simple-static-generation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["static-generation", "router"] }
|
||||
tracing-subscriber = "0.3.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
17
packages/static-generation/examples/simple/src/main.rs
Normal file
17
packages/static-generation/examples/simple/src/main.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
//! Static generation lets you pre-render your entire app to static files and then hydrate it on the client.
|
||||
use dioxus::prelude::*;
|
||||
|
||||
// Generate all routes and output them to the static path
|
||||
fn main() {
|
||||
launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
}
|
||||
}
|
189
packages/static-generation/src/config.rs
Normal file
189
packages/static-generation/src/config.rs
Normal file
|
@ -0,0 +1,189 @@
|
|||
//! Launch helper macros for fullstack apps
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Settings for a staticly generated site that may be hydrated in the browser
|
||||
pub struct Config {
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) output_dir: PathBuf,
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) index_html: Option<String>,
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) index_path: Option<PathBuf>,
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) map_path: Option<Box<dyn Fn(&str) -> PathBuf + Send + Sync + 'static>>,
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) root_id: Option<&'static str>,
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) additional_routes: Vec<String>,
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) github_pages: bool,
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
#[allow(unused)]
|
||||
pub(crate) web_cfg: dioxus_web::Config,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "server")]
|
||||
output_dir: PathBuf::from("./static"),
|
||||
#[cfg(feature = "server")]
|
||||
index_html: None,
|
||||
#[cfg(feature = "server")]
|
||||
index_path: None,
|
||||
#[cfg(feature = "server")]
|
||||
map_path: None,
|
||||
#[cfg(feature = "server")]
|
||||
root_id: None,
|
||||
#[cfg(feature = "server")]
|
||||
additional_routes: vec!["/".to_string()],
|
||||
#[cfg(feature = "server")]
|
||||
github_pages: false,
|
||||
#[cfg(feature = "web")]
|
||||
web_cfg: dioxus_web::Config::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This config intentionally leaves server options in even if the server feature is not enabled to make it easier to use the config without moving different parts of the builder under configs.
|
||||
impl Config {
|
||||
/// Create a new config for a static site generation app.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`.
|
||||
/// The function should return the path to the folder to store the index.html file in.
|
||||
///
|
||||
/// This method will only effect static site generation.
|
||||
#[allow(unused)]
|
||||
pub fn map_path<F: Fn(&str) -> PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
self.map_path = Some(Box::new(map_path));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the output directory for the static site generation. (defaults to ./static)
|
||||
/// This is the directory that will be used to store the generated static html files.
|
||||
///
|
||||
/// This method will only effect static site generation.
|
||||
#[allow(unused)]
|
||||
pub fn output_dir(mut self, output_dir: PathBuf) -> Self {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
self.output_dir = output_dir;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main)
|
||||
#[allow(unused)]
|
||||
pub fn root_id(mut self, root_id: &'static str) -> Self {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
self.root_id = Some(root_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the contents of the index.html file to be served. (precedence over index_path)
|
||||
///
|
||||
/// This method will only effect static site generation.
|
||||
#[allow(unused)]
|
||||
pub fn index_html(mut self, index_html: String) -> Self {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
self.index_html = Some(index_html);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
|
||||
///
|
||||
/// This method will only effect static site generation.
|
||||
#[allow(unused)]
|
||||
pub fn index_path(mut self, index_path: PathBuf) -> Self {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
self.index_path = Some(index_path);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a list of static routes that will be pre-rendered and served in addition to the static routes in the router.
|
||||
#[allow(unused)]
|
||||
pub fn additional_routes(mut self, mut routes: Vec<String>) -> Self {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
self.additional_routes.append(&mut routes);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// A preset for github pages. This will output your files in the `/docs` directory and set up a `404.html` file.
|
||||
pub fn github_pages(self) -> Self {
|
||||
#[allow(unused_mut)]
|
||||
let mut myself = self
|
||||
.additional_routes(vec!["/404".into()])
|
||||
.output_dir(PathBuf::from("./docs"));
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
myself.github_pages = true;
|
||||
}
|
||||
myself
|
||||
}
|
||||
|
||||
/// Set the web config.
|
||||
///
|
||||
/// This method will only effect the hydrated web renderer.
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
|
||||
pub fn web_cfg(self, web_cfg: dioxus_web::Config) -> Self {
|
||||
Self { web_cfg, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
impl Config {
|
||||
pub(crate) fn fullstack_template(
|
||||
&self,
|
||||
server_context: &dioxus_fullstack::prelude::DioxusServerContext,
|
||||
) -> impl dioxus_ssr::incremental::WrapBody {
|
||||
use dioxus_fullstack::prelude::{FullstackHTMLTemplate, ServeConfig};
|
||||
let mut cfg_builder = ServeConfig::builder();
|
||||
if let Some(index_html) = &self.index_html {
|
||||
cfg_builder = cfg_builder.index_html(index_html.clone());
|
||||
}
|
||||
if let Some(index_path) = &self.index_path {
|
||||
cfg_builder = cfg_builder.index_path(index_path.clone());
|
||||
}
|
||||
if let Some(root_id) = self.root_id {
|
||||
cfg_builder = cfg_builder.root_id(root_id);
|
||||
}
|
||||
let cfg = cfg_builder.build();
|
||||
|
||||
FullstackHTMLTemplate::new(&cfg, server_context)
|
||||
}
|
||||
|
||||
pub(crate) fn create_renderer(&mut self) -> dioxus_ssr::incremental::IncrementalRenderer {
|
||||
let mut builder = dioxus_ssr::incremental::IncrementalRenderer::builder()
|
||||
.static_dir(self.output_dir.clone())
|
||||
.pre_render(true);
|
||||
if let Some(map_path) = self.map_path.take() {
|
||||
builder = builder.map_path(map_path);
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
85
packages/static-generation/src/launch.rs
Normal file
85
packages/static-generation/src/launch.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
//! This module contains the `launch` function, which is the main entry point for dioxus fullstack
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
use dioxus_lib::prelude::{Element, VirtualDom};
|
||||
|
||||
pub use crate::Config;
|
||||
|
||||
/// Launch a fullstack app with the given root component, contexts, and config.
|
||||
#[allow(unused)]
|
||||
pub fn launch(
|
||||
root: fn() -> Element,
|
||||
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
platform_config: Config,
|
||||
) {
|
||||
let virtual_dom_factory = move || {
|
||||
let mut vdom = VirtualDom::new(root);
|
||||
for context in &contexts {
|
||||
vdom.insert_any_root_context(context());
|
||||
}
|
||||
vdom
|
||||
};
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
use axum::extract::Path;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use axum::ServiceExt;
|
||||
use dioxus_hot_reload::HotReloadRouterExt;
|
||||
use http::StatusCode;
|
||||
use tower_http::services::ServeDir;
|
||||
use tower_http::services::ServeFile;
|
||||
|
||||
let github_pages = platform_config.github_pages;
|
||||
let path = platform_config.output_dir.clone();
|
||||
crate::ssg::generate_static_site(root, platform_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Serve the program if we are running with cargo
|
||||
if std::env::var_os("CARGO").is_some() || std::env::var_os("DIOXUS_ACTIVE").is_some() {
|
||||
println!(
|
||||
"Serving static files from {} at http://127.0.0.1:8080",
|
||||
path.display()
|
||||
);
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
|
||||
let mut serve_dir =
|
||||
ServeDir::new(path.clone()).call_fallback_on_method_not_allowed(true);
|
||||
|
||||
let mut router = axum::Router::new().forward_cli_hot_reloading();
|
||||
|
||||
// If we are acting like github pages, we need to serve the 404 page if the user requests a directory that doesn't exist
|
||||
router = if github_pages {
|
||||
router.fallback_service(
|
||||
serve_dir.fallback(ServeFile::new(path.join("404/index.html"))),
|
||||
)
|
||||
} else {
|
||||
router.fallback_service(serve_dir.fallback(get(|| async move {
|
||||
"The requested path does not exist"
|
||||
.to_string()
|
||||
.into_response()
|
||||
})))
|
||||
};
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, router.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
let cfg = platform_config.web_cfg.hydrate(true);
|
||||
dioxus_web::launch::launch_virtual_dom(virtual_dom_factory(), cfg);
|
||||
}
|
||||
}
|
||||
}
|
8
packages/static-generation/src/lib.rs
Normal file
8
packages/static-generation/src/lib.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
mod config;
|
||||
pub use config::*;
|
||||
pub mod launch;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) mod ssg;
|
164
packages/static-generation/src/ssg.rs
Normal file
164
packages/static-generation/src/ssg.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
use dioxus_lib::prelude::*;
|
||||
use dioxus_router::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::Config;
|
||||
|
||||
fn server_context_for_route(route: &str) -> dioxus_fullstack::prelude::DioxusServerContext {
|
||||
use dioxus_fullstack::prelude::*;
|
||||
use std::sync::Arc;
|
||||
let request = http::Request::builder().uri(route).body(()).unwrap();
|
||||
let (parts, _) = request.into_parts();
|
||||
|
||||
DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)))
|
||||
}
|
||||
|
||||
/// Try to extract the site map by finding the root router that a component renders.
|
||||
fn extract_site_map(app: fn() -> Element) -> Option<&'static [SiteMapSegment]> {
|
||||
let mut vdom = VirtualDom::new(app);
|
||||
|
||||
vdom.rebuild_in_place();
|
||||
|
||||
vdom.in_runtime(|| {
|
||||
ScopeId::ROOT.in_runtime(|| dioxus_router::prelude::root_router().map(|r| r.site_map()))
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a static site from any fullstack app that uses the router.
|
||||
pub async fn generate_static_site(
|
||||
app: fn() -> Element,
|
||||
mut config: Config,
|
||||
) -> Result<(), IncrementalRendererError> {
|
||||
use tokio::task::block_in_place;
|
||||
|
||||
// Create the static output dir
|
||||
std::fs::create_dir_all(&config.output_dir)?;
|
||||
|
||||
let mut renderer = config.create_renderer();
|
||||
|
||||
let mut routes_to_render: HashSet<String> = config.additional_routes.iter().cloned().collect();
|
||||
if let Some(site_map) = block_in_place(|| extract_site_map(app)) {
|
||||
let flat_site_map = site_map.iter().flat_map(SiteMapSegment::flatten);
|
||||
for route in flat_site_map {
|
||||
let Some(static_route) = route
|
||||
.iter()
|
||||
.map(SegmentType::to_static)
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let url = format!("/{}", static_route.join("/"));
|
||||
|
||||
routes_to_render.insert(url);
|
||||
}
|
||||
} else {
|
||||
tracing::trace!("No site map found, rendering the additional routes");
|
||||
}
|
||||
|
||||
for url in routes_to_render {
|
||||
prerender_route(app, url, &mut renderer, &config).await?;
|
||||
}
|
||||
|
||||
// Copy over the web output dir into the static output dir
|
||||
let assets_path = dioxus_cli_config::CURRENT_CONFIG
|
||||
.as_ref()
|
||||
.map(|c| c.dioxus_config.application.out_dir.clone())
|
||||
.unwrap_or("./dist".into());
|
||||
|
||||
copy_static_files(&assets_path, &config.output_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_static_files(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
|
||||
let index_path = src.join("index.html");
|
||||
let mut queue = vec![src.to_path_buf()];
|
||||
while let Some(path) = queue.pop() {
|
||||
if path == index_path {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
for entry in fs::read_dir(&path).into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
queue.push(path);
|
||||
}
|
||||
} else {
|
||||
let output_location = dst.join(path.strip_prefix(src).unwrap());
|
||||
let parent = output_location.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(&path, output_location)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prerender_route(
|
||||
app: fn() -> Element,
|
||||
route: String,
|
||||
renderer: &mut dioxus_ssr::incremental::IncrementalRenderer,
|
||||
config: &Config,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
let context = server_context_for_route(&route);
|
||||
let wrapper = config.fullstack_template(&context);
|
||||
renderer
|
||||
.render(
|
||||
route,
|
||||
|| VirtualDom::new(app),
|
||||
&mut tokio::io::sink(),
|
||||
|vdom| {
|
||||
Box::pin(async move {
|
||||
with_server_context(context.clone(), || {
|
||||
tokio::task::block_in_place(|| vdom.rebuild_in_place());
|
||||
});
|
||||
ProvideServerContext::new(vdom.wait_for_suspense(), context).await;
|
||||
})
|
||||
},
|
||||
&wrapper,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_site_map_works() {
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
#[route("/about")]
|
||||
About {},
|
||||
}
|
||||
|
||||
fn Home() -> Element {
|
||||
rsx! { "Home" }
|
||||
}
|
||||
|
||||
fn About() -> Element {
|
||||
rsx! { "About" }
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
Other {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn Other() -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
let site_map = extract_site_map(app);
|
||||
assert_eq!(site_map, Some(Route::SITE_MAP));
|
||||
}
|
Loading…
Reference in a new issue