feat(fullstack): support wasm target
This commit is contained in:
1 parent
a454f2c7ac
commit
1f0e03ca19
9 files changed
+190
-128
No files matched your search
Generated
+1
@@ -2917,6 +2917,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -369,8 +369,8 @@ async fn start_server(
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::on_serve_start(_config)?;
|
||||
|
||||
// Parse address
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
// Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack)
|
||||
let addr = format!("[::]:{}", port).parse().unwrap();
|
||||
|
||||
// Open the browser
|
||||
if start_browser {
|
||||
|
||||
@@ -45,14 +45,13 @@ dioxus-mobile = { workspace = true, optional = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-futures = { workspace = true, optional = true }
|
||||
once_cell = "1.17.1"
|
||||
tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true }
|
||||
tokio-util = { version = "0.7.8", features = ["rt"], optional = true }
|
||||
anymap = { version = "0.12.1", optional = true }
|
||||
|
||||
serde = "1.0.159"
|
||||
serde_json = { version = "1.0.95", optional = true }
|
||||
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
|
||||
futures-util = { workspace = true, default-features = false, optional = true }
|
||||
futures-util = { workspace = true, default-features = false }
|
||||
ciborium = "0.2.1"
|
||||
base64 = "0.21.0"
|
||||
|
||||
@@ -66,12 +65,16 @@ web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document
|
||||
|
||||
dioxus-cli-config = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { workspace = true, features = ["rt", "sync"], optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
dioxus-hot-reload = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["hot-reload"]
|
||||
hot-reload = ["serde_json", "futures-util"]
|
||||
hot-reload = ["serde_json"]
|
||||
web = ["dioxus-web", "web-sys"]
|
||||
desktop = ["dioxus-desktop"]
|
||||
mobile = ["dioxus-mobile"]
|
||||
|
||||
@@ -19,7 +19,9 @@ pub mod warp_adapter;
|
||||
|
||||
use http::StatusCode;
|
||||
use server_fn::{Encoding, Payload};
|
||||
use std::future::Future;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::task::JoinError;
|
||||
|
||||
use crate::{
|
||||
layer::{BoxedService, Service},
|
||||
@@ -80,7 +82,7 @@ impl Service for ServerFnHandler {
|
||||
server_context,
|
||||
function,
|
||||
} = self.clone();
|
||||
Box::pin(async move {
|
||||
let f = async move {
|
||||
let query = req.uri().query().unwrap_or_default().as_bytes().to_vec();
|
||||
let (parts, body) = req.into_parts();
|
||||
let body = hyper::body::to_bytes(body).await?.to_vec();
|
||||
@@ -89,26 +91,22 @@ impl Service for ServerFnHandler {
|
||||
let parts = Arc::new(RwLock::new(parts));
|
||||
|
||||
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
|
||||
let pool = get_local_pool();
|
||||
let result = pool
|
||||
.spawn_pinned({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let server_function_future = function.call((), data);
|
||||
let server_function_future = ProvideServerContext::new(
|
||||
server_function_future,
|
||||
server_context.clone(),
|
||||
);
|
||||
server_function_future.await
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let result = spawn_platform({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let server_function_future = function.call((), data);
|
||||
let server_function_future =
|
||||
ProvideServerContext::new(server_function_future, server_context.clone());
|
||||
server_function_future.await
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let mut res = http::Response::builder();
|
||||
|
||||
// Set the headers from the server context
|
||||
@@ -147,7 +145,36 @@ impl Service for ServerFnHandler {
|
||||
res.body(data.into())?
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
Box::pin(f)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
use futures_util::future::FutureExt;
|
||||
|
||||
let result = tokio::task::spawn_local(f);
|
||||
let result = result.then(|f| async move { f.unwrap() });
|
||||
Box::pin(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_platform<F, Fut>(create_task: F) -> Result<<Fut as Future>::Output, JoinError>
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
F: Send + 'static,
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: Send + 'static,
|
||||
{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
get_local_pool().spawn_pinned(create_task).await
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Ok(create_task().await)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn launch(
|
||||
vdom
|
||||
};
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg(all(feature = "server", not(target_arch = "wasm32")))]
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
|
||||
@@ -7,13 +7,33 @@ use dioxus_ssr::{
|
||||
Renderer,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tokio::task::{spawn_blocking, JoinHandle};
|
||||
|
||||
use crate::prelude::*;
|
||||
use dioxus_lib::prelude::*;
|
||||
|
||||
fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: Send + 'static,
|
||||
{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
spawn_blocking(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(f())
|
||||
})
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
tokio::task::spawn_local(f())
|
||||
}
|
||||
}
|
||||
|
||||
enum SsrRendererPool {
|
||||
Renderer(RwLock<Vec<Renderer>>),
|
||||
Incremental(RwLock<Vec<dioxus_ssr::incremental::IncrementalRenderer>>),
|
||||
@@ -39,51 +59,41 @@ impl SsrRendererPool {
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
spawn_blocking(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(async move {
|
||||
let mut vdom = virtual_dom_factory();
|
||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context =
|
||||
SERVER_CONTEXT.with(|ctx| ctx.replace(server_context));
|
||||
// poll the future, which may call server_context()
|
||||
tracing::info!("Rebuilding vdom");
|
||||
let _ = vdom.rebuild(&mut NoOpMutations);
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
|
||||
spawn_platform(move || async move {
|
||||
let mut vdom = virtual_dom_factory();
|
||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(server_context));
|
||||
// poll the future, which may call server_context()
|
||||
tracing::info!("Rebuilding vdom");
|
||||
let _ = vdom.rebuild(&mut NoOpMutations);
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
|
||||
|
||||
if let Err(err) = wrapper.render_before_body(&mut *to) {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
if let Err(err) = renderer.render_to(&mut to, &vdom) {
|
||||
let _ = tx.send(Err(
|
||||
dioxus_ssr::incremental::IncrementalRendererError::RenderError(
|
||||
err,
|
||||
),
|
||||
));
|
||||
return;
|
||||
}
|
||||
if let Err(err) = wrapper.render_after_body(&mut *to) {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
match String::from_utf8(to.buffer) {
|
||||
Ok(html) => {
|
||||
let _ =
|
||||
tx.send(Ok((renderer, RenderFreshness::now(None), html)));
|
||||
}
|
||||
Err(err) => {
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(
|
||||
Box::new(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Err(err) = wrapper.render_before_body(&mut *to) {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
if let Err(err) = renderer.render_to(&mut to, &vdom) {
|
||||
let _ = tx.send(Err(
|
||||
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
|
||||
));
|
||||
return;
|
||||
}
|
||||
if let Err(err) = wrapper.render_after_body(&mut *to) {
|
||||
let _ = tx.send(Err(err));
|
||||
return;
|
||||
}
|
||||
match String::from_utf8(to.buffer) {
|
||||
Ok(html) => {
|
||||
let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
|
||||
}
|
||||
Err(err) => {
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err));
|
||||
}
|
||||
}
|
||||
});
|
||||
let (renderer, freshness, html) = rx.await.unwrap()?;
|
||||
pool.write().unwrap().push(renderer);
|
||||
@@ -98,53 +108,49 @@ impl SsrRendererPool {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
let server_context = server_context.clone();
|
||||
spawn_blocking(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(async move {
|
||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
||||
match renderer
|
||||
.render(
|
||||
route,
|
||||
virtual_dom_factory,
|
||||
&mut *to,
|
||||
|vdom| {
|
||||
Box::pin(async move {
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context = SERVER_CONTEXT
|
||||
.with(|ctx| ctx.replace(Box::new(server_context)));
|
||||
// poll the future, which may call server_context()
|
||||
tracing::info!("Rebuilding vdom");
|
||||
let _ = vdom.rebuild(&mut NoOpMutations);
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
|
||||
})
|
||||
},
|
||||
&wrapper,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(freshness) => {
|
||||
match String::from_utf8(to.buffer).map_err(|err| {
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(
|
||||
Box::new(err),
|
||||
)
|
||||
}) {
|
||||
Ok(html) => {
|
||||
let _ = tx.send(Ok((freshness, html)));
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.send(Err(err));
|
||||
}
|
||||
}
|
||||
spawn_platform(move || async move {
|
||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
||||
match renderer
|
||||
.render(
|
||||
route,
|
||||
virtual_dom_factory,
|
||||
&mut *to,
|
||||
|vdom| {
|
||||
Box::pin(async move {
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context = SERVER_CONTEXT
|
||||
.with(|ctx| ctx.replace(Box::new(server_context)));
|
||||
// poll the future, which may call server_context()
|
||||
tracing::info!("Rebuilding vdom");
|
||||
let _ = vdom.rebuild(&mut NoOpMutations);
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
|
||||
})
|
||||
},
|
||||
&wrapper,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(freshness) => {
|
||||
match String::from_utf8(to.buffer).map_err(|err| {
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
|
||||
err,
|
||||
))
|
||||
}) {
|
||||
Ok(html) => {
|
||||
let _ = tx.send(Ok((freshness, html)));
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.send(Err(err));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.send(Err(err));
|
||||
}
|
||||
}
|
||||
});
|
||||
let (freshness, html) = rx.await.unwrap()?;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use dioxus_lib::prelude::*;
|
||||
#[derive(Clone)]
|
||||
pub struct ServeConfigBuilder {
|
||||
pub(crate) root_id: Option<&'static str>,
|
||||
pub(crate) index_html: Option<String>,
|
||||
pub(crate) index_path: Option<PathBuf>,
|
||||
pub(crate) assets_path: Option<PathBuf>,
|
||||
pub(crate) incremental:
|
||||
@@ -44,6 +45,7 @@ impl ServeConfigBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
root_id: None,
|
||||
index_html: None,
|
||||
index_path: None,
|
||||
assets_path: None,
|
||||
incremental: None,
|
||||
@@ -56,6 +58,12 @@ impl ServeConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the contents of the index.html file to be served. (precedence over index_path)
|
||||
pub fn index_html(mut self, index_html: String) -> Self {
|
||||
self.index_html = Some(index_html);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
|
||||
pub fn index_path(mut self, index_path: PathBuf) -> Self {
|
||||
self.index_path = Some(index_path);
|
||||
@@ -90,8 +98,11 @@ impl ServeConfigBuilder {
|
||||
|
||||
let root_id = self.root_id.unwrap_or("main");
|
||||
|
||||
let index = load_index_html(index_path, root_id);
|
||||
let index_html = self
|
||||
.index_html
|
||||
.unwrap_or_else(|| load_index_path(index_path));
|
||||
|
||||
let index = load_index_html(index_html, root_id);
|
||||
ServeConfig {
|
||||
index,
|
||||
assets_path,
|
||||
@@ -100,13 +111,16 @@ impl ServeConfigBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_index_html(path: PathBuf, root_id: &'static str) -> IndexHtml {
|
||||
fn load_index_path(path: PathBuf) -> String {
|
||||
let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built.");
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)
|
||||
.expect("Failed to read index.html");
|
||||
contents
|
||||
}
|
||||
|
||||
fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
||||
let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."));
|
||||
|
||||
let post_main = post_main.split_once('>').unwrap_or_else(|| {
|
||||
|
||||
@@ -18,9 +18,15 @@ rustc-hash = "1.1.0"
|
||||
lru = "0.10.0"
|
||||
tracing = { workspace = true }
|
||||
http = "0.2.9"
|
||||
tokio = { version = "1.28", features = ["fs", "io-util"], optional = true }
|
||||
async-trait = "0.1.58"
|
||||
serde_json = { version = "1.0" }
|
||||
web-time = "1.0.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { version = "1.28", features = ["io-util"], optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.28", features = ["fs", "io-util"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
|
||||
@@ -23,8 +23,9 @@ pub use crate::incremental_cfg::*;
|
||||
pub struct IncrementalRenderer {
|
||||
pub(crate) static_dir: PathBuf,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) memory_cache:
|
||||
Option<lru::LruCache<String, (SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>>,
|
||||
pub(crate) memory_cache: Option<
|
||||
lru::LruCache<String, (web_time::SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>,
|
||||
>,
|
||||
pub(crate) invalidate_after: Option<Duration>,
|
||||
pub(crate) ssr_renderer: crate::Renderer,
|
||||
pub(crate) map_path: PathMapFn,
|
||||
@@ -98,22 +99,25 @@ impl IncrementalRenderer {
|
||||
route: String,
|
||||
html: Vec<u8>,
|
||||
) -> Result<RenderFreshness, IncrementalRendererError> {
|
||||
let file_path = self.route_as_path(&route);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let file_path = self.route_as_path(&route);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
let file = std::fs::File::create(file_path)?;
|
||||
let mut file = std::io::BufWriter::new(file);
|
||||
file.write_all(&html)?;
|
||||
}
|
||||
let file = std::fs::File::create(file_path)?;
|
||||
let mut file = std::io::BufWriter::new(file);
|
||||
file.write_all(&html)?;
|
||||
self.add_to_memory_cache(route, html);
|
||||
Ok(RenderFreshness::now(self.invalidate_after))
|
||||
}
|
||||
|
||||
fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) {
|
||||
if let Some(cache) = self.memory_cache.as_mut() {
|
||||
cache.put(route, (SystemTime::now(), html));
|
||||
cache.put(route, (web_time::SystemTime::now(), html));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +155,7 @@ impl IncrementalRenderer {
|
||||
}
|
||||
}
|
||||
// check the file cache
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(file_path) = self.find_file(&route) {
|
||||
if let Some(freshness) = file_path.freshness(self.invalidate_after) {
|
||||
if let Ok(file) = tokio::fs::File::open(file_path.full_path).await {
|
||||
|
||||
Reference in new issue
Block a user