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:
1 parent
460b70e0f0
commit
245003a5d4
59 files changed
+1507
-791
No files matched your search
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user