feat(fullstack): support wasm target

This commit is contained in:
Koji AGAWA committed 2024-02-17 05:12:33 +09:00
1 parent a454f2c7ac
commit 1f0e03ca19
9 files changed
+190 -128

No files matched your search

Generated
+1
View File
@@ -2917,6 +2917,7 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"web-time",
]
[[package]]
+2 -2
View File
@@ -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 {
+6 -3
View File
@@ -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"]
+49 -22
View File
@@ -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)
}
}
+1 -1
View File
@@ -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 {
+93 -87
View File
@@ -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()?;
+16 -2
View File
@@ -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(|| {
+7 -1
View File
@@ -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 }
+15 -10
View File
@@ -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 {