Преглед на файлове

feat(fullstack): support wasm target

Koji AGAWA преди 1 година
родител
ревизия
1f0e03ca19

+ 1 - 0
Cargo.lock

@@ -2917,6 +2917,7 @@ dependencies = [
  "thiserror",
  "tokio",
  "tracing",
+ "web-time",
 ]
 
 [[package]]

+ 2 - 2
packages/cli/src/server/web/mod.rs

@@ -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
packages/fullstack/Cargo.toml

@@ -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
packages/fullstack/src/adapters/mod.rs

@@ -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
packages/fullstack/src/launch.rs

@@ -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 {

+ 94 - 88
packages/fullstack/src/render.rs

@@ -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));
-
-                            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),
-                                    );
-                                }
-                            }
-                        });
+                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));
+                        }
+                    }
                 });
                 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
packages/fullstack/src/serve_config.rs

@@ -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
packages/ssr/Cargo.toml

@@ -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
packages/ssr/src/incremental.rs

@@ -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 {