* Fix server crash logging and opening fullstack apps * Fix race condition during SSG * move route prerendering into a separate stage * Fix static site generation with dx serve * only proxy the fullstack server if ssg is disabled * test nested-suspense with ssg in playwright
117 lines
4.7 KiB
Rust
117 lines
4.7 KiB
Rust
use anyhow::Context;
|
|
use dioxus_cli_config::{server_ip, server_port};
|
|
use futures_util::stream::FuturesUnordered;
|
|
use futures_util::StreamExt;
|
|
use std::{
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
path::Path,
|
|
time::Duration,
|
|
};
|
|
use tokio::process::Command;
|
|
|
|
pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Result<()> {
|
|
// Use the address passed in through environment variables or default to localhost:9999. We need
|
|
// to default to a value that is different than the CLI default address to avoid conflicts
|
|
let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
|
let port = server_port().unwrap_or(9999);
|
|
let fullstack_address = SocketAddr::new(ip, port);
|
|
let address = fullstack_address.ip().to_string();
|
|
let port = fullstack_address.port().to_string();
|
|
// Borrow port and address so we can easily moe them into multiple tasks below
|
|
let address = &address;
|
|
let port = &port;
|
|
|
|
tracing::info!("Running SSG at http://{address}:{port}");
|
|
|
|
// Run the server executable
|
|
let _child = Command::new(server_exe)
|
|
.env(dioxus_cli_config::SERVER_PORT_ENV, port)
|
|
.env(dioxus_cli_config::SERVER_IP_ENV, address)
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.kill_on_drop(true)
|
|
.spawn()?;
|
|
|
|
let reqwest_client = reqwest::Client::new();
|
|
// Borrow reqwest_client so we only move the reference into the futures
|
|
let reqwest_client = &reqwest_client;
|
|
|
|
// Get the routes from the `/static_routes` endpoint
|
|
let mut routes = None;
|
|
|
|
// The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay
|
|
const RETRY_ATTEMPTS: usize = 5;
|
|
for i in 0..=RETRY_ATTEMPTS {
|
|
let request = reqwest_client
|
|
.post(format!("http://{address}:{port}/api/static_routes"))
|
|
.send()
|
|
.await;
|
|
match request {
|
|
Ok(request) => {
|
|
routes = Some(request
|
|
.json::<Vec<String>>()
|
|
.await
|
|
.inspect(|text| tracing::debug!("Got static routes: {text:?}"))
|
|
.context("Failed to parse static routes from the server. Make sure your server function returns Vec<String> with the (default) json encoding")?);
|
|
break;
|
|
}
|
|
Err(err) => {
|
|
// If the request fails, try up to 5 times with a one second delay
|
|
// If it fails 5 times, return the error
|
|
if i == RETRY_ATTEMPTS {
|
|
return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec<String> of static routes.");
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
let routes = routes.expect(
|
|
"static routes should exist or an error should have been returned on the last attempt",
|
|
);
|
|
|
|
// Create a pool of futures that cache each route
|
|
let mut resolved_routes = routes
|
|
.into_iter()
|
|
.map(|route| async move {
|
|
tracing::info!("Rendering {route} for SSG");
|
|
// For each route, ping the server to force it to cache the response for ssg
|
|
let request = reqwest_client
|
|
.get(format!("http://{address}:{port}{route}"))
|
|
.header("Accept", "text/html")
|
|
.send()
|
|
.await?;
|
|
// If it takes longer than 30 seconds to resolve the route, log a warning
|
|
let warning_task = tokio::spawn({
|
|
let route = route.clone();
|
|
async move {
|
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
tracing::warn!("Route {route} has been rendering for 30 seconds");
|
|
}
|
|
});
|
|
// Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly
|
|
// because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write
|
|
// the final clean HTML to the disk automatically after the request completes.
|
|
let _html = request.text().await?;
|
|
|
|
// Cancel the warning task if it hasn't already run
|
|
warning_task.abort();
|
|
|
|
Ok::<_, reqwest::Error>(route)
|
|
})
|
|
.collect::<FuturesUnordered<_>>();
|
|
|
|
while let Some(route) = resolved_routes.next().await {
|
|
match route {
|
|
Ok(route) => tracing::debug!("ssg success: {route:?}"),
|
|
Err(err) => tracing::error!("ssg error: {err:?}"),
|
|
}
|
|
}
|
|
|
|
tracing::info!("SSG complete");
|
|
|
|
drop(_child);
|
|
|
|
Ok(())
|
|
}
|