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", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"web-time",
] ]
[[package]] [[package]]
+2 -2
View File
@@ -369,8 +369,8 @@ async fn start_server(
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
PluginManager::on_serve_start(_config)?; PluginManager::on_serve_start(_config)?;
// Parse address // Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack)
let addr = format!("0.0.0.0:{}", port).parse().unwrap(); let addr = format!("[::]:{}", port).parse().unwrap();
// Open the browser // Open the browser
if start_browser { if start_browser {
+6 -3
View File
@@ -45,14 +45,13 @@ dioxus-mobile = { workspace = true, optional = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-futures = { workspace = true, optional = true } tracing-futures = { workspace = true, optional = true }
once_cell = "1.17.1" 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 } tokio-util = { version = "0.7.8", features = ["rt"], optional = true }
anymap = { version = "0.12.1", optional = true } anymap = { version = "0.12.1", optional = true }
serde = "1.0.159" serde = "1.0.159"
serde_json = { version = "1.0.95", optional = true } serde_json = { version = "1.0.95", optional = true }
tokio-stream = { version = "0.1.12", features = ["sync"], 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" ciborium = "0.2.1"
base64 = "0.21.0" 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 } 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] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { workspace = true } dioxus-hot-reload = { workspace = true }
tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true }
[features] [features]
default = ["hot-reload"] default = ["hot-reload"]
hot-reload = ["serde_json", "futures-util"] hot-reload = ["serde_json"]
web = ["dioxus-web", "web-sys"] web = ["dioxus-web", "web-sys"]
desktop = ["dioxus-desktop"] desktop = ["dioxus-desktop"]
mobile = ["dioxus-mobile"] mobile = ["dioxus-mobile"]
+49 -22
View File
@@ -19,7 +19,9 @@ pub mod warp_adapter;
use http::StatusCode; use http::StatusCode;
use server_fn::{Encoding, Payload}; use server_fn::{Encoding, Payload};
use std::future::Future;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use tokio::task::JoinError;
use crate::{ use crate::{
layer::{BoxedService, Service}, layer::{BoxedService, Service},
@@ -80,7 +82,7 @@ impl Service for ServerFnHandler {
server_context, server_context,
function, function,
} = self.clone(); } = self.clone();
Box::pin(async move { let f = async move {
let query = req.uri().query().unwrap_or_default().as_bytes().to_vec(); let query = req.uri().query().unwrap_or_default().as_bytes().to_vec();
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let body = hyper::body::to_bytes(body).await?.to_vec(); 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)); 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 // 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 = spawn_platform({
let result = pool let function = function.clone();
.spawn_pinned({ let mut server_context = server_context.clone();
let function = function.clone(); server_context.parts = parts;
let mut server_context = server_context.clone(); move || async move {
server_context.parts = parts; let data = match function.encoding() {
move || async move { Encoding::Url | Encoding::Cbor => &body,
let data = match function.encoding() { Encoding::GetJSON | Encoding::GetCBOR => &query,
Encoding::Url | Encoding::Cbor => &body, };
Encoding::GetJSON | Encoding::GetCBOR => &query, let server_function_future = function.call((), data);
}; let server_function_future =
let server_function_future = function.call((), data); ProvideServerContext::new(server_function_future, server_context.clone());
let server_function_future = ProvideServerContext::new( server_function_future.await
server_function_future, }
server_context.clone(), })
); .await?;
server_function_future.await
}
})
.await?;
let mut res = http::Response::builder(); let mut res = http::Response::builder();
// Set the headers from the server context // Set the headers from the server context
@@ -147,7 +145,36 @@ impl Service for ServerFnHandler {
res.body(data.into())? 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 vdom
}; };
#[cfg(feature = "server")] #[cfg(all(feature = "server", not(target_arch = "wasm32")))]
tokio::runtime::Runtime::new() tokio::runtime::Runtime::new()
.unwrap() .unwrap()
.block_on(async move { .block_on(async move {
+93 -87
View File
@@ -7,13 +7,33 @@ use dioxus_ssr::{
Renderer, Renderer,
}; };
use serde::Serialize; use serde::Serialize;
use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
use std::sync::RwLock; use std::sync::RwLock;
use tokio::task::spawn_blocking; use tokio::task::{spawn_blocking, JoinHandle};
use crate::prelude::*; use crate::prelude::*;
use dioxus_lib::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 { enum SsrRendererPool {
Renderer(RwLock<Vec<Renderer>>), Renderer(RwLock<Vec<Renderer>>),
Incremental(RwLock<Vec<dioxus_ssr::incremental::IncrementalRenderer>>), Incremental(RwLock<Vec<dioxus_ssr::incremental::IncrementalRenderer>>),
@@ -39,51 +59,41 @@ impl SsrRendererPool {
let (tx, rx) = tokio::sync::oneshot::channel(); let (tx, rx) = tokio::sync::oneshot::channel();
spawn_blocking(move || { spawn_platform(move || async move {
tokio::runtime::Runtime::new() let mut vdom = virtual_dom_factory();
.expect("couldn't spawn runtime") let mut to = WriteBuffer { buffer: Vec::new() };
.block_on(async move { // before polling the future, we need to set the context
let mut vdom = virtual_dom_factory(); let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(server_context));
let mut to = WriteBuffer { buffer: Vec::new() }; // poll the future, which may call server_context()
// before polling the future, we need to set the context tracing::info!("Rebuilding vdom");
let prev_context = let _ = vdom.rebuild(&mut NoOpMutations);
SERVER_CONTEXT.with(|ctx| ctx.replace(server_context)); vdom.wait_for_suspense().await;
// poll the future, which may call server_context() tracing::info!("Suspense resolved");
tracing::info!("Rebuilding vdom"); // after polling the future, we need to restore the context
let _ = vdom.rebuild(&mut NoOpMutations); SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
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) { if let Err(err) = wrapper.render_before_body(&mut *to) {
let _ = tx.send(Err(err)); let _ = tx.send(Err(err));
return; return;
} }
if let Err(err) = renderer.render_to(&mut to, &vdom) { if let Err(err) = renderer.render_to(&mut to, &vdom) {
let _ = tx.send(Err( let _ = tx.send(Err(
dioxus_ssr::incremental::IncrementalRendererError::RenderError( dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
err, ));
), return;
)); }
return; if let Err(err) = wrapper.render_after_body(&mut *to) {
} let _ = tx.send(Err(err));
if let Err(err) = wrapper.render_after_body(&mut *to) { return;
let _ = tx.send(Err(err)); }
return; match String::from_utf8(to.buffer) {
} Ok(html) => {
match String::from_utf8(to.buffer) { let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
Ok(html) => { }
let _ = Err(err) => {
tx.send(Ok((renderer, RenderFreshness::now(None), html))); dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err));
} }
Err(err) => { }
dioxus_ssr::incremental::IncrementalRendererError::Other(
Box::new(err),
);
}
}
});
}); });
let (renderer, freshness, html) = rx.await.unwrap()?; let (renderer, freshness, html) = rx.await.unwrap()?;
pool.write().unwrap().push(renderer); pool.write().unwrap().push(renderer);
@@ -98,53 +108,49 @@ impl SsrRendererPool {
let (tx, rx) = tokio::sync::oneshot::channel(); let (tx, rx) = tokio::sync::oneshot::channel();
let server_context = server_context.clone(); let server_context = server_context.clone();
spawn_blocking(move || { spawn_platform(move || async move {
tokio::runtime::Runtime::new() let mut to = WriteBuffer { buffer: Vec::new() };
.expect("couldn't spawn runtime") match renderer
.block_on(async move { .render(
let mut to = WriteBuffer { buffer: Vec::new() }; route,
match renderer virtual_dom_factory,
.render( &mut *to,
route, |vdom| {
virtual_dom_factory, Box::pin(async move {
&mut *to, // before polling the future, we need to set the context
|vdom| { let prev_context = SERVER_CONTEXT
Box::pin(async move { .with(|ctx| ctx.replace(Box::new(server_context)));
// before polling the future, we need to set the context // poll the future, which may call server_context()
let prev_context = SERVER_CONTEXT tracing::info!("Rebuilding vdom");
.with(|ctx| ctx.replace(Box::new(server_context))); let _ = vdom.rebuild(&mut NoOpMutations);
// poll the future, which may call server_context() vdom.wait_for_suspense().await;
tracing::info!("Rebuilding vdom"); tracing::info!("Suspense resolved");
let _ = vdom.rebuild(&mut NoOpMutations); // after polling the future, we need to restore the context
vdom.wait_for_suspense().await; SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context));
tracing::info!("Suspense resolved"); })
// after polling the future, we need to restore the context },
SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); &wrapper,
}) )
}, .await
&wrapper, {
) Ok(freshness) => {
.await match String::from_utf8(to.buffer).map_err(|err| {
{ dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
Ok(freshness) => { err,
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)));
}) {
Ok(html) => {
let _ = tx.send(Ok((freshness, html)));
}
Err(err) => {
let _ = tx.send(Err(err));
}
}
} }
Err(err) => { Err(err) => {
let _ = tx.send(Err(err)); let _ = tx.send(Err(err));
} }
} }
}) }
Err(err) => {
let _ = tx.send(Err(err));
}
}
}); });
let (freshness, html) = rx.await.unwrap()?; let (freshness, html) = rx.await.unwrap()?;
+16 -2
View File
@@ -13,6 +13,7 @@ use dioxus_lib::prelude::*;
#[derive(Clone)] #[derive(Clone)]
pub struct ServeConfigBuilder { pub struct ServeConfigBuilder {
pub(crate) root_id: Option<&'static str>, pub(crate) root_id: Option<&'static str>,
pub(crate) index_html: Option<String>,
pub(crate) index_path: Option<PathBuf>, pub(crate) index_path: Option<PathBuf>,
pub(crate) assets_path: Option<PathBuf>, pub(crate) assets_path: Option<PathBuf>,
pub(crate) incremental: pub(crate) incremental:
@@ -44,6 +45,7 @@ impl ServeConfigBuilder {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
root_id: None, root_id: None,
index_html: None,
index_path: None, index_path: None,
assets_path: None, assets_path: None,
incremental: None, incremental: None,
@@ -56,6 +58,12 @@ impl ServeConfigBuilder {
self 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) /// 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 { pub fn index_path(mut self, index_path: PathBuf) -> Self {
self.index_path = Some(index_path); self.index_path = Some(index_path);
@@ -90,8 +98,11 @@ impl ServeConfigBuilder {
let root_id = self.root_id.unwrap_or("main"); 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 { ServeConfig {
index, index,
assets_path, 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 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(); let mut contents = String::new();
file.read_to_string(&mut contents) file.read_to_string(&mut contents)
.expect("Failed to read index.html"); .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 (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(|| { 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" lru = "0.10.0"
tracing = { workspace = true } tracing = { workspace = true }
http = "0.2.9" http = "0.2.9"
tokio = { version = "1.28", features = ["fs", "io-util"], optional = true }
async-trait = "0.1.58" async-trait = "0.1.58"
serde_json = { version = "1.0" } 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] [dev-dependencies]
dioxus = { workspace = true } dioxus = { workspace = true }
+15 -10
View File
@@ -23,8 +23,9 @@ pub use crate::incremental_cfg::*;
pub struct IncrementalRenderer { pub struct IncrementalRenderer {
pub(crate) static_dir: PathBuf, pub(crate) static_dir: PathBuf,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub(crate) memory_cache: pub(crate) memory_cache: Option<
Option<lru::LruCache<String, (SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>>, lru::LruCache<String, (web_time::SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>,
>,
pub(crate) invalidate_after: Option<Duration>, pub(crate) invalidate_after: Option<Duration>,
pub(crate) ssr_renderer: crate::Renderer, pub(crate) ssr_renderer: crate::Renderer,
pub(crate) map_path: PathMapFn, pub(crate) map_path: PathMapFn,
@@ -98,22 +99,25 @@ impl IncrementalRenderer {
route: String, route: String,
html: Vec<u8>, html: Vec<u8>,
) -> Result<RenderFreshness, IncrementalRendererError> { ) -> Result<RenderFreshness, IncrementalRendererError> {
let file_path = self.route_as_path(&route); #[cfg(not(target_arch = "wasm32"))]
if let Some(parent) = file_path.parent() { {
if !parent.exists() { let file_path = self.route_as_path(&route);
std::fs::create_dir_all(parent)?; 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); self.add_to_memory_cache(route, html);
Ok(RenderFreshness::now(self.invalidate_after)) Ok(RenderFreshness::now(self.invalidate_after))
} }
fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) { fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) {
if let Some(cache) = self.memory_cache.as_mut() { 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 // check the file cache
#[cfg(not(target_arch = "wasm32"))]
if let Some(file_path) = self.find_file(&route) { if let Some(file_path) = self.find_file(&route) {
if let Some(freshness) = file_path.freshness(self.invalidate_after) { if let Some(freshness) = file_path.freshness(self.invalidate_after) {
if let Ok(file) = tokio::fs::File::open(file_path.full_path).await { if let Ok(file) = tokio::fs::File::open(file_path.full_path).await {