mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Merge branch 'master' into pr/atty303/1948
This commit is contained in:
commit
c4a4a31eec
78 changed files with 1277 additions and 4465 deletions
43
.github/workflows/main.yml
vendored
43
.github/workflows/main.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
cache-all-crates: "true"
|
||||
save-if: ${{ github.ref == 'refs/heads/master' }}
|
||||
- uses: ilammy/setup-nasm@v1
|
||||
- run: cargo check --all --examples --tests
|
||||
- run: cargo check --all --examples --tests --all-features --all-targets
|
||||
|
||||
test:
|
||||
if: github.event.pull_request.draft == false
|
||||
|
@ -110,45 +110,7 @@ jobs:
|
|||
with:
|
||||
cache-all-crates: "true"
|
||||
save-if: ${{ github.ref == 'refs/heads/master' }}
|
||||
- run: cargo clippy --workspace --examples --tests -- -D warnings
|
||||
|
||||
# We removed most unsafe that we can, and using nightly doubles our cache size
|
||||
# miri:
|
||||
# if: github.event.pull_request.draft == false
|
||||
# name: Miri
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# CARGO_UNSTABLE_SPARSE_REGISTRY: 'true'
|
||||
# RUSTFLAGS: -Dwarnings
|
||||
# RUST_BACKTRACE: 1
|
||||
# MIRIFLAGS: -Zmiri-tag-gc=1
|
||||
# # Change to specific Rust release to pin
|
||||
# rust_stable: stable
|
||||
# rust_nightly: nightly-2023-11-16
|
||||
# rust_clippy: 1.70.0
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: ilammy/setup-nasm@v1
|
||||
# - name: Install Rust ${{ env.rust_nightly }}
|
||||
# uses: dtolnay/rust-toolchain@master
|
||||
# with:
|
||||
# toolchain: ${{ env.rust_nightly }}
|
||||
# components: miri
|
||||
# - uses: Swatinem/rust-cache@v2
|
||||
# with:
|
||||
# cache-all-crates: "true"
|
||||
# save-if: ${{ github.ref == 'refs/heads/master' }}
|
||||
# - name: miri
|
||||
# # Many of tests in tokio/tests and doctests use #[tokio::test] or
|
||||
# # #[tokio::main] that calls epoll_create1 that Miri does not support.
|
||||
# # run: cargo miri test --features full --lib --no-fail-fast
|
||||
# run: |
|
||||
# cargo miri test --package dioxus-core -- --exact --nocapture
|
||||
# cargo miri test --package dioxus-native-core --test miri_native -- --exact --nocapture
|
||||
# env:
|
||||
# MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
|
||||
# PROPTEST_CASES: 10
|
||||
- run: cargo clippy --workspace --examples --tests --all-features --all-targets -- -D warnings
|
||||
|
||||
playwright:
|
||||
if: github.event.pull_request.draft == false
|
||||
|
@ -200,7 +162,6 @@ jobs:
|
|||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
|
||||
matrix_test:
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
if: github.event.pull_request.draft == false
|
||||
|
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
@ -4,5 +4,7 @@
|
|||
"editor.formatOnSave": false
|
||||
},
|
||||
"rust-analyzer.check.workspace": true,
|
||||
"rust-analyzer.checkOnSave.allTargets": false,
|
||||
"rust-analyzer.check.features": "all",
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
"rust-analyzer.check.allTargets": true
|
||||
}
|
||||
|
|
1775
Cargo.lock
generated
1775
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
25
Cargo.toml
25
Cargo.toml
|
@ -35,10 +35,9 @@ members = [
|
|||
"packages/server-macro",
|
||||
"packages/fullstack/examples/axum-hello-world",
|
||||
"packages/fullstack/examples/axum-router",
|
||||
"packages/fullstack/examples/axum-streaming",
|
||||
"packages/fullstack/examples/axum-desktop",
|
||||
"packages/fullstack/examples/axum-auth",
|
||||
"packages/fullstack/examples/salvo-hello-world",
|
||||
"packages/fullstack/examples/warp-hello-world",
|
||||
"packages/fullstack/examples/static-hydrated",
|
||||
# Full project examples
|
||||
"examples/tailwind",
|
||||
|
@ -88,6 +87,7 @@ dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
|
|||
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
|
||||
tracing = "0.1.37"
|
||||
tracing-futures = "0.2.5"
|
||||
toml = "0.8"
|
||||
tokio = "1.28"
|
||||
slab = "0.4.2"
|
||||
futures-channel = "0.3.21"
|
||||
|
@ -105,6 +105,22 @@ manganis-cli-support = { git = "https://github.com/DioxusLabs/collect-assets", r
|
|||
] }
|
||||
manganis = { git = "https://github.com/DioxusLabs/collect-assets", rev = "f982698" }
|
||||
|
||||
lru = "0.12.2"
|
||||
async-trait = "0.1.77"
|
||||
axum = "0.7.0"
|
||||
axum-server = "0.6.0"
|
||||
tower = "0.4.13"
|
||||
http = "1.0.0"
|
||||
tower-http = "0.5.1"
|
||||
hyper = "1.0.0"
|
||||
hyper-rustls = "0.26.0"
|
||||
serde_json = "1.0.61"
|
||||
serde = "1.0.61"
|
||||
axum_session = "0.12.1"
|
||||
axum_session_auth = "0.12.1"
|
||||
axum-extra = "0.9.2"
|
||||
reqwest = "0.11.24"
|
||||
|
||||
# This is a "virtual package"
|
||||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||
[package]
|
||||
|
@ -125,7 +141,6 @@ publish = false
|
|||
manganis = { workspace = true, optional = true}
|
||||
reqwest = { version = "0.11.9", features = ["json"], optional = true}
|
||||
http-range = {version = "0.1.5", optional = true }
|
||||
warp = { version = "0.3.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true, features = ["router"] }
|
||||
|
@ -149,13 +164,11 @@ tokio = { version = "1.16.1", features = ["full"] }
|
|||
liveview = ["dioxus/liveview"]
|
||||
fullstack = ["dioxus/fullstack"]
|
||||
axum = ["dioxus/axum"]
|
||||
salvo = ["dioxus/salvo"]
|
||||
rocket = ["dioxus/rocket"]
|
||||
server = ["dioxus/axum"]
|
||||
default = ["dioxus/desktop"]
|
||||
web = ["dioxus/web"]
|
||||
collect-assets = ["manganis"]
|
||||
http = ["reqwest", "http-range", "warp"]
|
||||
http = ["reqwest", "http-range"]
|
||||
|
||||
[[example]]
|
||||
name = "login_form"
|
||||
|
|
|
@ -21,7 +21,7 @@ fn app() -> Element {
|
|||
let prompt = prompt.peek().clone();
|
||||
let number_of_images = n_image.peek().clone();
|
||||
|
||||
if (api_key.is_empty() || prompt.is_empty() || number_of_images.is_empty()) {
|
||||
if api_key.is_empty() || prompt.is_empty() || number_of_images.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
# Signals
|
||||
|
||||
Signals provide a way of forcing updates directly through Dioxus without having to go through the diffing phase.
|
||||
|
||||
When diffing is too slow for your use-case, signals can be faster. Signals run at a higher priority than regular diffing, acting as a hint to Dioxus that a signal update needs to take precedence over a subtree update. This can be useful in real-time systems where getting data from a websocket to the screen ASAP is extremely important.
|
||||
|
||||
- High
|
||||
- Medium
|
||||
- Low
|
||||
|
||||
## Signals:
|
||||
|
||||
Producer -> Receiver
|
||||
|
||||
- The Dioxus VirtualDOM provides built-in receivers for signals.
|
||||
- Elements themselves act as receivers.
|
||||
- Any use of a signal schedules the current element and its children for updates.
|
||||
- Attributes are valid receivers
|
||||
- Text nodes are valid receivers
|
||||
- Receivers may not be passed into child components (must be de-referenced)
|
||||
- When receivers are derefed in a component's properties, the props will be updated in place and the component will re-render with the new value.
|
||||
|
||||
```rust
|
||||
let sig = use_signal(|| 0);
|
||||
|
||||
// any updates to the signal will cause the child to re-render completely
|
||||
Comp {
|
||||
prop: *sig
|
||||
}
|
||||
```
|
||||
|
||||
Using 3 separate signals
|
||||
|
||||
```rust
|
||||
let width = use_signal(|| 0);
|
||||
|
||||
cx.request_next_frame(move |frame| async {
|
||||
sig1 += 1;
|
||||
frame.again();
|
||||
})
|
||||
|
||||
div {
|
||||
h2 { "{sig1}" }
|
||||
h3 { "{sig2}" }
|
||||
h4 { "{sig3}" }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Subtree memoization
|
||||
|
||||
The rsx! macro needs to be *really* smart. If it detects that no dynamics are pumped into the macro, then it opts to use the "const" flavors of the element build functions we know and love. This has to be done at build time rather than runtime since components may return basically anything. Using the const flavor enables is_static which encourages Dioxus to do a ptr compare instead of a value compare to short circuit through the diffing. Due to const folding in Rust, entire subtrees can be ruled out at compile time.
|
||||
|
||||
It would be interesting to fix the issue of dynamic subtrees by hashing each structure (or just the const structures) or the macro call itself. That way, each call gets its own identifier and we can make sure that two unique structures have different IDs and aren't just opaque to dioxus.
|
||||
|
||||
```rust
|
||||
let s1 = LazyNodes::new("1", move |_| {
|
||||
if rand() {
|
||||
f.element()
|
||||
} else {
|
||||
f.element()
|
||||
}
|
||||
});
|
||||
let s2 = LazyNodes::new("1", move |f| {
|
||||
if rand() {
|
||||
f.element()
|
||||
} else {
|
||||
f.element()
|
||||
}
|
||||
});
|
||||
// produces the same ID with different structures
|
||||
// perhaps just make this
|
||||
```
|
|
@ -1,138 +0,0 @@
|
|||
# Dioxus v0.1.0
|
||||
|
||||
Welcome to the first iteration of the Dioxus Virtual DOM! This release brings support for:
|
||||
|
||||
- Web via Wasm
|
||||
- Desktop via webview integration
|
||||
- Server-rendering with custom ToString implementation
|
||||
- State management
|
||||
- Build CLI
|
||||
- Foundational hooks
|
||||
- Context API
|
||||
- Basic suspense
|
||||
- Controlled components
|
||||
|
||||
---
|
||||
|
||||
## Project: Initial VDOM support (TBD)
|
||||
|
||||
> Get the initial VDom + Event System + Patching + Diffing + Component framework up and running
|
||||
> Get a demo working using just the web
|
||||
|
||||
- [x] (Core) Migrate virtual node into new VNode type
|
||||
- [x] (Core) Arena allocate VNodes
|
||||
- [x] (Core) Allow VNodes to borrow arena contents
|
||||
- [x] (Core) Introduce the VDOM and patch API for 3rd party renderers
|
||||
- [x] (Core) Implement lifecycle
|
||||
- [x] (Core) Implement an event system
|
||||
- [x] (Core) Implement child nodes, scope creation
|
||||
- [x] (Core) Implement dirty tagging and compression
|
||||
|
||||
## Project: QOL
|
||||
|
||||
> Make it easier to write components
|
||||
|
||||
- [x] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed/alloced)
|
||||
- [x] (Macro) Tweak component syntax to accept a new custom element
|
||||
- [ ] (Macro) Allow components to specify their props as function args
|
||||
|
||||
## Project: Hooks + Context + Subscriptions (TBD)
|
||||
|
||||
> Implement the foundations for state management
|
||||
|
||||
- [x] Implement context object
|
||||
- [x] Implement use_state (rewrite to use the use_reducer api like rei)
|
||||
- [x] Implement use_ref
|
||||
- [x] Implement use_context (only the API, not the state management solution)
|
||||
- [ ] Implement use_reducer (WIP)
|
||||
|
||||
## Project: String Render (TBD)
|
||||
|
||||
> Implement a light-weight string renderer with basic caching
|
||||
|
||||
- [x] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}"
|
||||
- [x] (SSR) Implement stateful 3rd party string renderer
|
||||
|
||||
## Project: Web_sys renderer (TBD)
|
||||
|
||||
- [x] WebSys edit interpreter
|
||||
- [x] Event system using async channels
|
||||
- [ ] Implement conversion of all event types into synthetic events
|
||||
|
||||
## Project: Web-View 🤲 🍨
|
||||
|
||||
> Proof of concept: stream render edits from server to client
|
||||
|
||||
- [x] Prove that the diffing and patching framework can support patch streaming
|
||||
|
||||
## Project: Examples
|
||||
|
||||
> Get _all_ the examples
|
||||
|
||||
- [ ] (Examples) Tide example with templating
|
||||
|
||||
## Project: State management
|
||||
|
||||
> Get some global state management installed with the hooks + context API
|
||||
|
||||
## Project: Concurrency (TBD)
|
||||
|
||||
> Ensure the concurrency model works well, play with lifetimes to check if it can be multithreaded + halted
|
||||
> ?
|
||||
|
||||
## Project: Mobile exploration
|
||||
|
||||
## Project: Live-View 🤲 🍨
|
||||
|
||||
> Combine the server and client into a single file :)
|
||||
|
||||
## Project: Sanitization (TBD)
|
||||
|
||||
> Improve code health
|
||||
|
||||
- [ ] (Macro) Clippy sanity for html macro
|
||||
- [ ] (Macro) Error sanitization
|
||||
|
||||
## Outstanding todos:
|
||||
|
||||
> anything missed so far
|
||||
|
||||
- [x] keys on components
|
||||
- [x] Allow paths for components
|
||||
- [x] todo mvc
|
||||
- [x] Tweak macro parsing for better errors
|
||||
- [x] dirty tagging, compression
|
||||
- [x] code health
|
||||
- [x] static str slice optimization
|
||||
- [x] name spacing so svg works
|
||||
- [x] A handful of svg elements are automatically namespaced
|
||||
- [ ] Allow hierarchical namespacing (all children share a parent's namespace) - TBD in macro impl
|
||||
- [ ] fix keys on elements
|
||||
- [ ] controlled components (kinda tuff since we need all these different platforms)
|
||||
- [ ] Their own crate
|
||||
- [ ] Re-exported through the `dioxus` crate (not core)
|
||||
- [ ] Hooks
|
||||
- [ ] Re-exported through the `dioxus` crate (not essential to core virtualdom)
|
||||
- [x] fragments
|
||||
- [x] pass-thru components
|
||||
|
||||
## Less-essential todos
|
||||
|
||||
- [ ] HTML doesn't require strings between elements (copy-paste from internet)
|
||||
- [ ] Beef up the dioxus CLI tool to report build progress
|
||||
- [ ] Extract arena logic out for better safety guarantees
|
||||
- [ ] Extract BumpFrame logic out for better safety guarantees
|
||||
- [ ] make SSR follow HTML spec
|
||||
- [ ] MIRI tests
|
||||
- [ ] all synthetic events filled out
|
||||
- [ ] double check event targets and stuff
|
||||
- [ ] Documentation overhaul
|
||||
- [ ] Website
|
||||
|
||||
lower priority features
|
||||
|
||||
- [x] Attributes on elements should implement format_args instead of string fmt
|
||||
- [ ] node refs (postpone for future release?)
|
||||
- [ ] styling built-in (future release?)
|
||||
- [ ] key handler?
|
||||
- [ ] FC macro?
|
|
@ -12,7 +12,7 @@ keywords = ["react", "gui", "cli", "dioxus", "wasm"]
|
|||
clap = { version = "4.2", features = ["derive"], optional = true }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
toml = { version = "0.5.8", optional = true }
|
||||
toml = { workspace = true, optional = true }
|
||||
cargo_toml = { version = "0.16.0", optional = true }
|
||||
once_cell = "1.18.0"
|
||||
tracing = { workspace = true }
|
||||
|
|
|
@ -21,7 +21,7 @@ log = "0.4.14"
|
|||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
toml = "0.8.8"
|
||||
toml = {workspace = true}
|
||||
fs_extra = "1.2.0"
|
||||
cargo_toml = "0.18.0"
|
||||
futures-util = { workspace = true }
|
||||
|
@ -32,22 +32,24 @@ tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
|
|||
atty = "0.2.14"
|
||||
chrono = "0.4.19"
|
||||
anyhow = "1"
|
||||
hyper = "0.14.17"
|
||||
hyper-rustls = "0.23.2"
|
||||
hyper = { workspace = true }
|
||||
hyper-util = "0.1.3"
|
||||
hyper-rustls = { workspace = true }
|
||||
indicatif = "0.17.5"
|
||||
subprocess = "0.2.9"
|
||||
rayon = "1.8.0"
|
||||
|
||||
axum = { version = "0.5.1", features = ["ws", "headers"] }
|
||||
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
|
||||
tower-http = { version = "0.2.2", features = ["full"] }
|
||||
headers = "0.3.7"
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
axum-server = { workspace = true, features = ["tls-rustls"] }
|
||||
axum-extra = { workspace = true, features = ["typed-header"] }
|
||||
tower-http = { workspace = true, features = ["full"] }
|
||||
|
||||
headers = "0.3.7"
|
||||
walkdir = "2"
|
||||
|
||||
# tools download
|
||||
dirs = "5.0.1"
|
||||
reqwest = { version = "0.11", features = [
|
||||
reqwest = { workspace = true, features = [
|
||||
"rustls-tls",
|
||||
"stream",
|
||||
"trust-dns",
|
||||
|
@ -56,7 +58,7 @@ reqwest = { version = "0.11", features = [
|
|||
flate2 = "1.0.22"
|
||||
tar = "0.4.38"
|
||||
zip = "0.6.2"
|
||||
tower = "0.4.12"
|
||||
tower = { workspace = true }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
# plugin packages
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use crate::assets::AssetConfigDropGuard;
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::plugin::PluginManager;
|
||||
use crate::server::fullstack;
|
||||
use dioxus_cli_config::Platform;
|
||||
|
||||
|
@ -55,7 +53,7 @@ impl Build {
|
|||
crate_config.set_cargo_args(self.build.cargo_args.clone());
|
||||
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
// let _ = crate::plugin::PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
let build_result = match platform {
|
||||
Platform::Web => {
|
||||
|
@ -116,7 +114,7 @@ impl Build {
|
|||
file.write_all(temp.as_bytes())?;
|
||||
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
// let _ = crate::plugin::PluginManager::on_build_finish(&crate_config, &platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5,9 +5,6 @@ use anyhow::Context;
|
|||
use clap::Parser;
|
||||
use dioxus_cli::*;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use dioxus_cli::plugin::PluginManager;
|
||||
|
||||
use Commands::*;
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||
|
@ -92,6 +89,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
DioxusConfig::default()
|
||||
});
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use dioxus_cli::plugin::PluginManager;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.context(error_wrapper("Plugin system initialization failed"))?;
|
||||
|
|
|
@ -45,7 +45,7 @@ impl UserData for PluginCommander {
|
|||
if cmd.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let cmd_name = cmd.get(0).unwrap();
|
||||
let cmd_name = cmd.first().unwrap();
|
||||
let mut command = Command::new(cmd_name);
|
||||
let t = cmd
|
||||
.iter()
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
use crate::server::HotReloadState;
|
||||
use axum::{
|
||||
extract::{ws::Message, WebSocketUpgrade},
|
||||
response::IntoResponse,
|
||||
Extension, TypedHeader,
|
||||
Extension,
|
||||
};
|
||||
|
||||
use crate::server::HotReloadState;
|
||||
|
||||
pub async fn hot_reload_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<HotReloadState>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
|
|
|
@ -8,8 +8,8 @@ use crate::{
|
|||
BuildResult, Result,
|
||||
};
|
||||
use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
body::Body,
|
||||
extract::{ws::Message, Extension, WebSocketUpgrade},
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderName, HeaderValue},
|
||||
|
@ -287,20 +287,18 @@ async fn setup_router(
|
|||
let mut response = if file_service_config.dioxus_config.web.watcher.index_on_404
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Full::from(
|
||||
let body = Body::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(file_service_config.out_dir().join("index.html"))
|
||||
.ok()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|err| match err {})
|
||||
.boxed();
|
||||
);
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
response.into_response()
|
||||
};
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
|
@ -324,7 +322,7 @@ async fn setup_router(
|
|||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
|error: std::convert::Infallible| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
|
@ -335,7 +333,7 @@ async fn setup_router(
|
|||
router = if let Some(base_path) = config.dioxus_config.web.app.base_path.clone() {
|
||||
let base_path = format!("/{}", base_path.trim_matches('/'));
|
||||
Router::new()
|
||||
.nest(&base_path, axum::routing::any_service(router))
|
||||
.route(&base_path, axum::routing::any_service(router))
|
||||
.fallback(get(move || {
|
||||
let base_path = base_path.clone();
|
||||
async move { format!("Outside of the base path: {}", base_path) }
|
||||
|
@ -388,9 +386,8 @@ async fn start_server(
|
|||
.await?
|
||||
}
|
||||
None => {
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, router.into_make_service()).await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,7 +415,6 @@ fn get_ip() -> Option<String> {
|
|||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
use crate::Result;
|
||||
use dioxus_cli_config::WebProxyConfig;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{http::StatusCode, routing::any, Router};
|
||||
use hyper::{Request, Response, Uri};
|
||||
use hyper_util::{
|
||||
client::legacy::{self, connect::HttpConnector},
|
||||
rt::TokioExecutor,
|
||||
};
|
||||
|
||||
use axum::body::Body as MyBody;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProxyClient {
|
||||
inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
|
||||
inner: legacy::Client<hyper_rustls::HttpsConnector<HttpConnector>, MyBody>,
|
||||
url: Uri,
|
||||
}
|
||||
|
||||
|
@ -15,19 +21,17 @@ impl ProxyClient {
|
|||
fn new(url: Uri) -> Self {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.unwrap()
|
||||
.https_or_http()
|
||||
.enable_http1()
|
||||
.build();
|
||||
Self {
|
||||
inner: hyper::Client::builder().build(https),
|
||||
inner: legacy::Client::builder(TokioExecutor::new()).build(https),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
&self,
|
||||
mut req: Request<hyper::body::Body>,
|
||||
) -> Result<Response<hyper::body::Body>> {
|
||||
async fn send(&self, mut req: Request<MyBody>) -> Result<Response<hyper::body::Incoming>> {
|
||||
let mut uri_parts = req.uri().clone().into_parts();
|
||||
uri_parts.authority = self.url.authority().cloned();
|
||||
uri_parts.scheme = self.url.scheme().cloned();
|
||||
|
@ -35,7 +39,7 @@ impl ProxyClient {
|
|||
self.inner
|
||||
.request(req)
|
||||
.await
|
||||
.map_err(crate::error::Error::ProxyRequestError)
|
||||
.map_err(|err| crate::error::Error::Other(anyhow!(err)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +53,7 @@ impl ProxyClient {
|
|||
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
let url: Uri = proxy.backend.parse()?;
|
||||
let path = url.path().to_string();
|
||||
let trimmed_path = path.trim_end_matches('/');
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
|
||||
if trimmed_path.is_empty() {
|
||||
return Err(crate::Error::ProxySetupError(format!(
|
||||
|
@ -61,14 +65,11 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
|||
|
||||
let client = ProxyClient::new(url);
|
||||
|
||||
// We also match everything after the path using a wildcard matcher.
|
||||
let wildcard_client = client.clone();
|
||||
|
||||
router = router.route(
|
||||
// Always remove trailing /'s so that the exact route
|
||||
// matches.
|
||||
trimmed_path,
|
||||
any(move |req| async move {
|
||||
&format!("/*{}", trimmed_path.trim_end_matches('/')),
|
||||
any(move |req: Request<MyBody>| async move {
|
||||
client
|
||||
.send(req)
|
||||
.await
|
||||
|
@ -76,19 +77,6 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
|||
}),
|
||||
);
|
||||
|
||||
// Wildcard match anything else _after_ the backend URL's path.
|
||||
// Note that we know `path` ends with a trailing `/` in this branch,
|
||||
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
|
||||
let wildcard = format!("{}/*proxywildcard", trimmed_path);
|
||||
router = router.route(
|
||||
&wildcard,
|
||||
any(move |req| async move {
|
||||
wildcard_client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
|
@ -97,30 +85,47 @@ mod test {
|
|||
|
||||
use super::*;
|
||||
|
||||
use axum::{extract::Path, Router};
|
||||
use axum::Router;
|
||||
use axum_server::{Handle, Server};
|
||||
|
||||
fn setup_servers(
|
||||
mut config: WebProxyConfig,
|
||||
) -> (
|
||||
tokio::task::JoinHandle<()>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
String,
|
||||
) {
|
||||
let backend_router = Router::new().route(
|
||||
"/*path",
|
||||
any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
|
||||
);
|
||||
let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(backend_router.into_make_service());
|
||||
let backend_addr = backend_server.local_addr();
|
||||
let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
|
||||
async fn setup_servers(mut config: WebProxyConfig) -> String {
|
||||
let backend_router =
|
||||
Router::new().route(
|
||||
"/*path",
|
||||
any(|request: axum::extract::Request| async move {
|
||||
format!("backend: {}", request.uri())
|
||||
}),
|
||||
);
|
||||
|
||||
// The API backend server
|
||||
let backend_handle_handle = Handle::new();
|
||||
let backend_handle_handle_ = backend_handle_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
Server::bind("127.0.0.1:0".parse().unwrap())
|
||||
.handle(backend_handle_handle_)
|
||||
.serve(backend_router.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Set the user's config to this dummy API we just built so we can test it
|
||||
let backend_addr = backend_handle_handle.listening().await.unwrap();
|
||||
config.backend = format!("http://{}{}", backend_addr, config.backend);
|
||||
|
||||
// Now set up our actual filesystem server
|
||||
let router = super::add_proxy(Router::new(), &config);
|
||||
let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(router.unwrap().into_make_service());
|
||||
let server_addr = server.local_addr();
|
||||
let server_handle = tokio::spawn(async move { server.await.unwrap() });
|
||||
(backend_handle, server_handle, server_addr.to_string())
|
||||
let server_handle_handle = Handle::new();
|
||||
let server_handle_handle_ = server_handle_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
Server::bind("127.0.0.1:0".parse().unwrap())
|
||||
.handle(server_handle_handle_)
|
||||
.serve(router.unwrap().into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Expose *just* the fileystem web server's address
|
||||
server_handle_handle.listening().await.unwrap().to_string()
|
||||
}
|
||||
|
||||
async fn test_proxy_requests(path: String) {
|
||||
|
@ -132,42 +137,38 @@ mod test {
|
|||
// So in day to day usage, use `http://localhost:8000/api` instead!
|
||||
backend: path,
|
||||
};
|
||||
let (backend_handle, server_handle, server_addr) = setup_servers(config);
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let server_addr = setup_servers(config).await;
|
||||
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
reqwest::get(format!("http://{}/api", server_addr))
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap(),
|
||||
"backend: /api"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api/", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
reqwest::get(format!("http://{}/api/", server_addr))
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap(),
|
||||
"backend: /api/"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(
|
||||
format!("http://{}/api/subpath", server_addr)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
reqwest::get(format!("http://{server_addr}/api/subpath"))
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap(),
|
||||
"backend: /api/subpath"
|
||||
);
|
||||
backend_handle.abort();
|
||||
server_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
@ -49,17 +49,12 @@ mobile = ["dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobi
|
|||
web = ["dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", "dioxus-router?/web"]
|
||||
ssr = ["dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
|
||||
liveview = ["dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
|
||||
axum = ["dioxus-fullstack?/axum", "ssr", "dioxus-liveview?/axum"]
|
||||
salvo = ["dioxus-fullstack?/salvo", "ssr", "dioxus-liveview?/salvo"]
|
||||
warp = ["dioxus-fullstack?/warp", "ssr", "dioxus-liveview?/warp"]
|
||||
rocket = ["dioxus-liveview?/rocket"]
|
||||
axum = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"]
|
||||
tui = ["dioxus-tui", "dioxus-config-macro/tui"]
|
||||
|
||||
# This feature just disables the no-renderer-enabled warning
|
||||
third-party-renderer = []
|
||||
|
||||
# This feature enables some nightly flags that make it more clear what structs/methods are available in each feature
|
||||
nightly-doc = []
|
||||
|
||||
[dev-dependencies]
|
||||
futures-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
|
|
@ -5,8 +5,8 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
let liveview_renderers = ["liveview", "axum", "salvo", "warp", "rocket"];
|
||||
let fullstack_renderers = ["axum", "salvo", "warp"];
|
||||
let liveview_renderers = ["liveview", "axum"];
|
||||
let fullstack_renderers = ["axum"];
|
||||
let client_renderers = ["desktop", "mobile", "web", "tui"];
|
||||
let client_renderer_selected = client_renderers
|
||||
.iter()
|
||||
|
|
|
@ -38,7 +38,7 @@ impl LaunchBuilder {
|
|||
|
||||
/// Launch your web application.
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
|
||||
pub fn web() -> LaunchBuilder<dioxus_web::Config, UnsendContext> {
|
||||
LaunchBuilder {
|
||||
launch_fn: dioxus_web::launch::launch,
|
||||
|
@ -49,7 +49,7 @@ impl LaunchBuilder {
|
|||
|
||||
/// Launch your desktop application.
|
||||
#[cfg(feature = "desktop")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
|
||||
pub fn desktop() -> LaunchBuilder<dioxus_desktop::Config, UnsendContext> {
|
||||
LaunchBuilder {
|
||||
launch_fn: dioxus_desktop::launch::launch,
|
||||
|
@ -60,7 +60,7 @@ impl LaunchBuilder {
|
|||
|
||||
/// Launch your fullstack application.
|
||||
#[cfg(feature = "fullstack")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
|
||||
pub fn fullstack() -> LaunchBuilder<dioxus_fullstack::Config, SendContext> {
|
||||
LaunchBuilder {
|
||||
launch_fn: dioxus_fullstack::launch::launch,
|
||||
|
@ -71,7 +71,7 @@ impl LaunchBuilder {
|
|||
|
||||
/// Launch your fullstack application.
|
||||
#[cfg(feature = "mobile")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "mobile")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
|
||||
pub fn mobile() -> LaunchBuilder<dioxus_mobile::Config, UnsendContext> {
|
||||
LaunchBuilder {
|
||||
launch_fn: dioxus_mobile::launch::launch,
|
||||
|
@ -81,7 +81,7 @@ impl LaunchBuilder {
|
|||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "tui")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
|
||||
/// Launch your tui application
|
||||
pub fn tui() -> LaunchBuilder<dioxus_tui::Config, UnsendContext> {
|
||||
LaunchBuilder {
|
||||
|
@ -232,28 +232,28 @@ pub fn launch(app: fn() -> Element) {
|
|||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
|
||||
/// Launch your web application without any additional configuration. See [`LaunchBuilder`] for more options.
|
||||
pub fn launch_web(app: fn() -> Element) {
|
||||
LaunchBuilder::web().launch(app)
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
|
||||
/// Launch your desktop application without any additional configuration. See [`LaunchBuilder`] for more options.
|
||||
pub fn launch_desktop(app: fn() -> Element) {
|
||||
LaunchBuilder::desktop().launch(app)
|
||||
}
|
||||
|
||||
#[cfg(feature = "fullstack")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
|
||||
/// Launch your fullstack application without any additional configuration. See [`LaunchBuilder`] for more options.
|
||||
pub fn launch_fullstack(app: fn() -> Element) {
|
||||
LaunchBuilder::fullstack().launch(app)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "tui")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
|
||||
/// Launch your tui application without any additional configuration. See [`LaunchBuilder`] for more options.
|
||||
pub fn launch_tui(app: fn() -> Element) {
|
||||
LaunchBuilder::tui().launch(app)
|
||||
|
|
|
@ -1,114 +1,115 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![cfg_attr(any(docsrs, feature = "nightly-doc"), feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use dioxus_core;
|
||||
|
||||
#[cfg(feature = "launch")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "launch")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||
mod launch;
|
||||
|
||||
#[cfg(feature = "hooks")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "hooks")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
|
||||
pub use dioxus_hooks as hooks;
|
||||
|
||||
#[cfg(feature = "signals")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "signals")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "signals")))]
|
||||
pub use dioxus_signals as signals;
|
||||
|
||||
pub mod events {
|
||||
#[cfg(feature = "html")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||
pub use dioxus_html::prelude::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||
pub use dioxus_html as html;
|
||||
|
||||
#[cfg(feature = "macro")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "macro")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
|
||||
pub use dioxus_core_macro as core_macro;
|
||||
|
||||
pub mod prelude {
|
||||
#[cfg(feature = "launch")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "launch")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||
pub use crate::launch::*;
|
||||
|
||||
#[cfg(feature = "hooks")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "hooks")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
|
||||
pub use crate::hooks::*;
|
||||
|
||||
#[cfg(feature = "signals")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "signals")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "signals")))]
|
||||
pub use dioxus_signals::*;
|
||||
|
||||
pub use dioxus_core::prelude::*;
|
||||
|
||||
#[cfg(feature = "macro")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "macro")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
|
||||
#[allow(deprecated)]
|
||||
pub use dioxus_core_macro::{component, format_args_f, inline_props, render, rsx, Props};
|
||||
|
||||
#[cfg(feature = "launch")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "launch")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||
pub use dioxus_config_macro::*;
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||
pub use dioxus_html as dioxus_elements;
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "html")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||
pub use dioxus_elements::{prelude::*, GlobalAttributes, SvgAttributes};
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "hot-reload"))]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "hot-reload")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "hot-reload")))]
|
||||
pub use dioxus_hot_reload::{self, hot_reload_init};
|
||||
|
||||
pub use dioxus_core;
|
||||
|
||||
#[cfg(feature = "fullstack")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
|
||||
pub use dioxus_fullstack::prelude::*;
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
||||
pub use dioxus_router;
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
||||
pub use dioxus_router::prelude::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
|
||||
pub use dioxus_web as web;
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
||||
pub use dioxus_router as router;
|
||||
|
||||
#[cfg(feature = "fullstack")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "fullstack")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
|
||||
pub use dioxus_fullstack as fullstack;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
|
||||
pub use dioxus_desktop as desktop;
|
||||
|
||||
#[cfg(feature = "mobile")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "mobile")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
|
||||
pub use dioxus_desktop as mobile;
|
||||
|
||||
#[cfg(feature = "liveview")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "liveview")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "liveview")))]
|
||||
pub use dioxus_liveview as liveview;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "tui")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tui")))]
|
||||
pub use dioxus_tui as tui;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "ssr")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "ssr")))]
|
||||
pub use dioxus_ssr as ssr;
|
||||
|
|
|
@ -12,26 +12,19 @@ resolver = "2"
|
|||
|
||||
[dependencies]
|
||||
# server functions
|
||||
server_fn = { version = "0.5.2", default-features = false }
|
||||
dioxus_server_macro = { workspace = true }
|
||||
|
||||
# warp
|
||||
warp = { version = "0.3.5", features = ["compression-gzip"], optional = true }
|
||||
server_fn = { version = "0.6.5", features = ["json", "url", "browser"], default-features = false }
|
||||
dioxus_server_macro = { workspace = true, version = "0.6.5", default-features = false }
|
||||
|
||||
# axum
|
||||
axum = { version = "0.6.1", features = ["ws", "macros"], default-features = false, optional = true }
|
||||
tower-http = { version = "0.4.0", optional = true, features = ["fs", "compression-gzip"] }
|
||||
|
||||
# salvo
|
||||
salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] }
|
||||
http-body-util = { version = "0.1.0-rc.2", optional = true }
|
||||
axum = { workspace = true, features = ["ws", "macros"], default-features = false, optional = true }
|
||||
tower-http = { workspace = true, optional = true, features = ["fs", "compression-gzip"] }
|
||||
|
||||
dioxus-lib = { workspace = true }
|
||||
|
||||
# Dioxus + SSR
|
||||
dioxus-ssr = { workspace = true, optional = true }
|
||||
hyper = { version = "0.14.25", optional = true }
|
||||
http = { version = "0.2.9", optional = true }
|
||||
hyper = { workspace = true, optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
|
||||
# Web Integration
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
|
@ -59,7 +52,7 @@ pin-project = { version = "1.1.2", optional = true }
|
|||
thiserror = { workspace = true, optional = true }
|
||||
async-trait = "0.1.71"
|
||||
bytes = "1.4.0"
|
||||
tower = { version = "0.4.13", features = ["util"], optional = true }
|
||||
tower = { workspace = true, features = ["util"], optional = true }
|
||||
tower-layer = { version = "0.3.2", optional = true }
|
||||
web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
|
||||
|
||||
|
@ -78,11 +71,24 @@ hot-reload = ["serde_json"]
|
|||
web = ["dioxus-web", "web-sys"]
|
||||
desktop = ["dioxus-desktop"]
|
||||
mobile = ["dioxus-mobile"]
|
||||
warp = ["dep:warp", "server"]
|
||||
axum = ["dep:axum", "tower-http", "server"]
|
||||
salvo = ["dep:salvo", "server", "http-body-util"]
|
||||
server = ["server_fn/ssr", "dioxus_server_macro/server", "tokio", "tokio-util", "tokio-stream", "dioxus-ssr", "dioxus-ssr/incremental", "tower", "hyper", "http", "tower-layer", "anymap", "tracing-futures", "pin-project", "thiserror", "dioxus-cli-config"]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
# This feature enables some nightly flags that make it more clear what structs/methods are available in each feature
|
||||
nightly-doc = []
|
||||
axum = ["dep:axum", "tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"]
|
||||
server = [
|
||||
"server_fn/ssr",
|
||||
"dioxus_server_macro/server",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tokio-stream",
|
||||
"dioxus-ssr",
|
||||
"dioxus-ssr/incremental",
|
||||
"tower",
|
||||
"hyper",
|
||||
"http",
|
||||
"tower-layer",
|
||||
"anymap",
|
||||
"tracing-futures",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
"dioxus-cli-config"
|
||||
]
|
||||
|
|
|
@ -37,51 +37,43 @@ Full stack Dioxus in under 50 lines of code
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::prelude::*;
|
||||
|
||||
// On the web, run our client code
|
||||
#[cfg(feature = "web")]
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
dioxus_web::launch_with_props(
|
||||
app,
|
||||
get_root_props_from_document().unwrap_or_default(),
|
||||
dioxus_web::Config::new().hydrate(true),
|
||||
);
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
warp::serve(
|
||||
// Automatically handles server side rendering, hot reloading intigration, and hosting server functions
|
||||
serve_dioxus_application(
|
||||
"",
|
||||
ServerConfig::new(app, ()),
|
||||
)
|
||||
)
|
||||
.run(([127, 0, 0, 1], 8080))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// On the server, run a simple warp server
|
||||
#[cfg(feature = "server")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Automatically handles server side rendering, hot reloading intigration, and hosting server functions
|
||||
warp::serve(serve_dioxus_application("", ServerConfig::new(app, ())))
|
||||
.run(([127, 0, 0, 1], 8080))
|
||||
.await;
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let meaning = use_signal(|| None);
|
||||
|
||||
rsx! {
|
||||
h1 { "Meaning of life: {meaning:?}" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![meaning];
|
||||
async move {
|
||||
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
|
||||
meaning.set(data);
|
||||
}
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
|
||||
meaning.set(data);
|
||||
}
|
||||
},
|
||||
"Run a server function"
|
||||
}
|
||||
"Server said: {meaning:?}"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This code will only run on the server
|
||||
#[server(GetMeaning)]
|
||||
#[server]
|
||||
async fn get_meaning(of: String) -> Result<Option<u32>, ServerFnError> {
|
||||
Ok(of.contains("life").then(|| 42))
|
||||
}
|
||||
|
|
|
@ -8,13 +8,11 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
||||
dioxus = { workspace = true }
|
||||
dioxus = { features = ["fullstack"], workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
axum = { version = "0.6.12", optional = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = "1.0.159"
|
||||
execute = "0.2.12"
|
||||
tower-http = { version = "0.4.1", features = ["auth"], optional = true }
|
||||
tower-http = { workspace = true, features = ["auth"], optional = true }
|
||||
simple_logger = { version = "4.2.0", optional = true }
|
||||
async-trait = { version = "0.1.71", optional = true }
|
||||
sqlx = { version = "0.7.0", features = [
|
||||
|
@ -26,17 +24,21 @@ sqlx = { version = "0.7.0", features = [
|
|||
"tls-rustls",
|
||||
"runtime-tokio",
|
||||
], optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
tower = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.159"
|
||||
execute = "0.2.12"
|
||||
anyhow = "1.0.71"
|
||||
http = { version = "0.2.9", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
|
||||
|
||||
[dependencies.axum_session]
|
||||
version = "0.3.0"
|
||||
workspace = true
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
[dependencies.axum_session_auth]
|
||||
version = "0.3.0"
|
||||
workspace = true
|
||||
features = ["sqlite-rustls"]
|
||||
optional = true
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
|||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
// Hydrate the application on the client
|
||||
dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
dioxus_web::launch::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
|
@ -44,14 +44,14 @@ fn main() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
//Create the Database table for storing our Session Data.
|
||||
session_store.initiate().await.unwrap();
|
||||
User::create_user_tables(&pool).await;
|
||||
|
||||
// build our application with some routes
|
||||
let app = Router::new()
|
||||
// Server side render the application, serve static assets, and register server functions
|
||||
.serve_dioxus_application("", ServerConfig::new(app, ()))
|
||||
.serve_dioxus_application(ServeConfig::builder().build(), || {
|
||||
VirtualDom::new(app)
|
||||
})
|
||||
.layer(
|
||||
axum_session_auth::AuthSessionLayer::<
|
||||
crate::auth::User,
|
||||
|
@ -65,9 +65,9 @@ fn main() {
|
|||
|
||||
// run it
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
|
|
@ -7,8 +7,8 @@ publish = false
|
|||
[lib]
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true }
|
||||
axum = { version = "0.6.12", optional = true }
|
||||
dioxus = { workspace = true, features = ["launch", "fullstack"] }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = "1.0.159"
|
||||
|
||||
|
|
|
@ -9,5 +9,7 @@ fn main() {
|
|||
// Set the url of the server where server functions are hosted.
|
||||
#[cfg(not(feature = "server"))]
|
||||
dioxus::fullstack::prelude::server_fn::set_server_url("http://127.0.0.1:8080");
|
||||
dioxus::desktop::launch(app)
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
dioxus::prelude::launch_desktop(app)
|
||||
}
|
||||
|
|
|
@ -5,20 +5,23 @@
|
|||
|
||||
use axum_desktop::*;
|
||||
use dioxus::prelude::*;
|
||||
use server_fn::axum::register_explicit;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
|
||||
let _ = PostServerData::register_explicit();
|
||||
let _ = GetServerData::register_explicit();
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(
|
||||
axum::Router::new()
|
||||
.register_server_fns("")
|
||||
.into_make_service(),
|
||||
)
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.01:8080")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
register_explicit::<PostServerData>();
|
||||
register_explicit::<GetServerData>();
|
||||
|
||||
axum::serve(
|
||||
listener,
|
||||
axum::Router::new()
|
||||
.register_server_fns()
|
||||
.into_make_service(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ publish = false
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["router"] }
|
||||
axum = { version = "0.6.12", optional = true }
|
||||
dioxus = { workspace = true, features = ["fullstack", "router"] }
|
||||
axum = { workspace = true, optional = true }
|
||||
tokio = {workspace = true, features = ["full"], optional = true }
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
|
||||
|
@ -16,3 +16,4 @@ serde = { version = "1.0.159", features = ["derive"] }
|
|||
default = []
|
||||
server = ["axum", "dioxus/axum"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
|
|
|
@ -7,21 +7,17 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let config = LaunchBuilder::fullstack();
|
||||
#[cfg(feature = "server")]
|
||||
config
|
||||
.incremental(
|
||||
IncrementalRendererConfig::default()
|
||||
.invalidate_after(std::time::Duration::from_secs(120)),
|
||||
)
|
||||
.launch();
|
||||
let cfg = server_only!(dioxus::fullstack::Config::new().incremental(
|
||||
IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)),
|
||||
));
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
config.launch(|| {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
});
|
||||
LaunchBuilder::fullstack().with_cfg(cfg).launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "warp-hello-world"
|
||||
name = "axum-streaming"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
@ -9,12 +9,16 @@ publish = false
|
|||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
serde = "1.0.159"
|
||||
simple_logger = "4.2.0"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = { workspace = true }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = "0.3.17"
|
||||
reqwest = "0.11.18"
|
||||
futures = "0.3.30"
|
||||
tokio = { workspace = true, optional = true }
|
||||
futures-util.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/warp"]
|
||||
server = ["dioxus/axum", "tokio"]
|
||||
web = ["dioxus/web"]
|
41
packages/fullstack/examples/axum-streaming/src/main.rs
Normal file
41
packages/fullstack/examples/axum-streaming/src/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use dioxus::prelude::*;
|
||||
use futures::StreamExt;
|
||||
use server_fn::codec::{StreamingText, TextStream};
|
||||
|
||||
fn app() -> Element {
|
||||
let mut response = use_signal(String::new);
|
||||
|
||||
rsx! {
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
response.write().clear();
|
||||
if let Ok(stream) = test_stream().await {
|
||||
response.write().push_str("Stream started\n");
|
||||
let mut stream = stream.into_inner();
|
||||
while let Some(Ok(text)) = stream.next().await {
|
||||
response.write().push_str(&text);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Start stream"
|
||||
}
|
||||
"{response}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server(output = StreamingText)]
|
||||
pub async fn test_stream() -> Result<TextStream, ServerFnError> {
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
let _ = tx.unbounded_send(Ok("Hello, world!".to_string()));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(TextStream::new(rx))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
launch(app)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
[package]
|
||||
name = "salvo-hello-world"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-web = { workspace = true, features=["hydrate"], optional = true }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
serde = "1.0.159"
|
||||
salvo = { version = "0.63.0", optional = true }
|
||||
execute = "0.2.12"
|
||||
reqwest = "0.11.18"
|
||||
simple_logger = "4.2.0"
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = "0.3.17"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["salvo", "dioxus-fullstack/salvo"]
|
||||
web = ["dioxus-web"]
|
|
@ -1,57 +0,0 @@
|
|||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform fullstack
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_fullstack::{launch, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn app() -> Element {
|
||||
let state = use_server_future(move || async move { get_server_data().await.unwrap() })?;
|
||||
|
||||
let mut count = use_signal(|| 0);
|
||||
let mut text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
div { "Server state: {state.value().unwrap()}" }
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
},
|
||||
"Run a server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server(PostServerData)]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server(GetServerData)]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
LaunchBuilder::fullstack().launch(app);
|
||||
}
|
|
@ -14,16 +14,25 @@ use serde::{Deserialize, Serialize};
|
|||
#[cfg(feature = "server")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pre_cache_static_routes_with_props(
|
||||
&ServerConfig::new_with_router(
|
||||
dioxus_fullstack::router::FullstackRouterConfig::<Route>::default(),
|
||||
)
|
||||
.assets_path("docs")
|
||||
.incremental(IncrementalRendererConfig::default().static_dir("docs"))
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let wrapper = DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
</head>
|
||||
<body>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
};
|
||||
let mut renderer = IncrementalRenderer::builder().build();
|
||||
pre_cache_static_routes::<Route, _>(&mut renderer, &wrapper)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Hydrate the page
|
||||
|
@ -37,13 +46,11 @@ fn main() {
|
|||
);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "web", feature = "server")))]
|
||||
fn main() {}
|
||||
|
||||
#[derive(Clone, Routable, Debug, PartialEq, Serialize, Deserialize)]
|
||||
enum Route {
|
||||
#[route("/")]
|
||||
Home {},
|
||||
|
||||
#[route("/blog")]
|
||||
Blog,
|
||||
}
|
||||
|
@ -72,10 +79,7 @@ fn Home() -> Element {
|
|||
let text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
Link {
|
||||
to: Route::Blog {},
|
||||
"Go to blog"
|
||||
}
|
||||
Link { to: Route::Blog {}, "Go to blog" }
|
||||
div {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
dist
|
||||
target
|
||||
static
|
||||
.dioxus
|
|
@ -1,54 +0,0 @@
|
|||
//! Run with:
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --platform fullstack
|
||||
//! ```
|
||||
|
||||
#![allow(non_snake_case, unused)]
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn app() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
let text = use_signal(|| "...".to_string());
|
||||
|
||||
rsx! {
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
button {
|
||||
onclick: move |_| {
|
||||
to_owned![text];
|
||||
async move {
|
||||
if let Ok(data) = get_server_data().await {
|
||||
println!("Client received: {}", data);
|
||||
text.set(data.clone());
|
||||
post_server_data(data).await.unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
"Run a server function!"
|
||||
}
|
||||
"Server said: {text}"
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
|
||||
println!("Server received: {}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
tracing_wasm::set_as_global_default();
|
||||
#[cfg(feature = "server")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
launch(app);
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
//! # Adapters
|
||||
//! Adapters for different web frameworks.
|
||||
//!
|
||||
//! Each adapter provides a set of utilities that is ergonomic to use with the framework.
|
||||
//!
|
||||
//! Each framework has utilies for some or all of the following:
|
||||
//! - Server functions
|
||||
//! - A generic way to register server functions
|
||||
//! - A way to register server functions with a custom handler that allows users to pass in a custom [`crate::server_context::DioxusServerContext`] based on the state of the server framework.
|
||||
//! - A way to register static WASM files that is accepts [`crate::serve_config::ServeConfig`]
|
||||
//! - A hot reloading web socket that intigrates with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload)
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum_adapter;
|
||||
// #[cfg(feature = "salvo")]
|
||||
// pub mod salvo_adapter;
|
||||
#[cfg(feature = "warp")]
|
||||
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},
|
||||
prelude::{DioxusServerContext, ProvideServerContext},
|
||||
};
|
||||
|
||||
/// Create a server function handler with the given server context and server function.
|
||||
pub fn server_fn_service(
|
||||
context: DioxusServerContext,
|
||||
function: server_fn::ServerFnTraitObj<()>,
|
||||
) -> crate::layer::BoxedService {
|
||||
let prefix = function.prefix().to_string();
|
||||
let url = function.url().to_string();
|
||||
if let Some(middleware) = crate::server_fn::MIDDLEWARE.get(&(&prefix, &url)) {
|
||||
let mut service = BoxedService(Box::new(ServerFnHandler::new(context, function)));
|
||||
for middleware in middleware {
|
||||
service = middleware.layer(service);
|
||||
}
|
||||
service
|
||||
} else {
|
||||
BoxedService(Box::new(ServerFnHandler::new(context, function)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
|
||||
pub struct ServerFnHandler {
|
||||
server_context: DioxusServerContext,
|
||||
function: server_fn::ServerFnTraitObj<()>,
|
||||
}
|
||||
|
||||
impl ServerFnHandler {
|
||||
/// Create a new server function handler with the given server context and server function.
|
||||
pub fn new(
|
||||
server_context: impl Into<DioxusServerContext>,
|
||||
function: server_fn::ServerFnTraitObj<()>,
|
||||
) -> Self {
|
||||
let server_context = server_context.into();
|
||||
Self {
|
||||
server_context,
|
||||
function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Service for ServerFnHandler {
|
||||
fn run(
|
||||
&mut self,
|
||||
req: http::Request<hyper::body::Body>,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
|
||||
> + Send,
|
||||
>,
|
||||
> {
|
||||
let Self {
|
||||
server_context,
|
||||
function,
|
||||
} = self.clone();
|
||||
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();
|
||||
let headers = &parts.headers;
|
||||
let accept_header = headers.get("Accept").cloned();
|
||||
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 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
|
||||
let parts = server_context.response_parts().unwrap();
|
||||
*res.headers_mut().expect("empty headers should be valid") = parts.headers.clone();
|
||||
|
||||
let serialized = result?;
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok());
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
|
||||
Ok(match serialized {
|
||||
Payload::Binary(data) => {
|
||||
res = res.header("Content-Type", "application/cbor");
|
||||
res.body(data.into())?
|
||||
}
|
||||
Payload::Url(data) => {
|
||||
res = res.header(
|
||||
"Content-Type",
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
);
|
||||
res.body(data.into())?
|
||||
}
|
||||
Payload::Json(data) => {
|
||||
res = res.header("Content-Type", "application/json");
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_local_pool() -> tokio_util::task::LocalPoolHandle {
|
||||
use once_cell::sync::OnceCell;
|
||||
static LOCAL_POOL: OnceCell<tokio_util::task::LocalPoolHandle> = OnceCell::new();
|
||||
LOCAL_POOL
|
||||
.get_or_init(|| {
|
||||
tokio_util::task::LocalPoolHandle::new(
|
||||
std::thread::available_parallelism()
|
||||
.map(Into::into)
|
||||
.unwrap_or(1),
|
||||
)
|
||||
})
|
||||
.clone()
|
||||
}
|
|
@ -1,581 +0,0 @@
|
|||
//! Dioxus utilities for the [Salvo](https://salvo.rs) server framework.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust
|
||||
//! #![allow(non_snake_case)]
|
||||
//! use dioxus_lib::prelude::*;
|
||||
//! use dioxus_fullstack::prelude::*;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! #[cfg(feature = "web")]
|
||||
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
//! #[cfg(feature = "server")]
|
||||
//! {
|
||||
//! use salvo::prelude::*;
|
||||
//! tokio::runtime::Runtime::new()
|
||||
//! .unwrap()
|
||||
//! .block_on(async move {
|
||||
//! let router =
|
||||
//! Router::new().serve_dioxus_application("", ServerConfig::new(app, ()));
|
||||
//! Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
//! .serve(router)
|
||||
//! .await;
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn app() -> Element {
|
||||
//! let text = use_signal(|| "...".to_string());
|
||||
//!
|
||||
//! rsx! {
|
||||
//! button {
|
||||
//! onclick: move |_| {
|
||||
//! to_owned![text];
|
||||
//! async move {
|
||||
//! if let Ok(data) = get_server_data().await {
|
||||
//! text.set(data);
|
||||
//! }
|
||||
//! }
|
||||
//! },
|
||||
//! "Run a server function"
|
||||
//! }
|
||||
//! "Server said: {text}"
|
||||
//! })
|
||||
//! }
|
||||
//!
|
||||
//! #[server(GetServerData)]
|
||||
//! async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
//! Ok("Hello from the server!".to_string())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use dioxus_lib::prelude::*;
|
||||
use http_body_util::{BodyExt, Limited};
|
||||
use hyper::body::Body as HyperBody;
|
||||
use hyper::StatusCode;
|
||||
use salvo::{
|
||||
async_trait, handler,
|
||||
http::{
|
||||
cookie::{Cookie, CookieJar},
|
||||
ParseError,
|
||||
},
|
||||
serve_static::{StaticDir, StaticFile},
|
||||
Depot, Error as SalvoError, FlowCtrl, Handler, Request, Response, Router,
|
||||
};
|
||||
use server_fn::{Encoding, ServerFunctionRegistry};
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
layer::Service, prelude::*, render::SSRState, serve_config::ServeConfig,
|
||||
server_fn::DioxusServerFnRegistry, server_fn_service,
|
||||
};
|
||||
|
||||
type HyperRequest = hyper::Request<hyper::Body>;
|
||||
type HyperResponse = hyper::Response<HyperBody>;
|
||||
|
||||
/// A extension trait with utilities for integrating Dioxus with your Salvo router.
|
||||
pub trait DioxusRouterExt {
|
||||
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use salvo::prelude::*;
|
||||
/// use std::{net::TcpListener, sync::Arc};
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// struct ServerFunctionHandler {
|
||||
/// server_fn: server_fn::ServerFnTraitObj<()>,
|
||||
/// }
|
||||
///
|
||||
/// #[handler]
|
||||
/// impl ServerFunctionHandler {
|
||||
/// async fn handle(
|
||||
/// &self,
|
||||
/// req: &mut Request,
|
||||
/// depot: &mut Depot,
|
||||
/// res: &mut Response,
|
||||
/// flow: &mut FlowCtrl,
|
||||
/// ) {
|
||||
/// // Add the headers to server context
|
||||
/// ServerFnHandler::new((req.headers().clone(),), self.server_fn.clone())
|
||||
/// .handle(req, depot, res, flow)
|
||||
/// .await
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let router = Router::new()
|
||||
/// .register_server_fns_with_handler("", |func| {
|
||||
/// ServerFnHandler::new(DioxusServerContext::default(), func)
|
||||
/// });
|
||||
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
/// .serve(router)
|
||||
/// .await;
|
||||
/// }
|
||||
/// ```
|
||||
fn register_server_fns_with_handler<H>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
handler: impl Fn(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler + 'static;
|
||||
|
||||
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use salvo::prelude::*;
|
||||
/// use std::{net::TcpListener, sync::Arc};
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let router = Router::new()
|
||||
/// .register_server_fns("");
|
||||
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
/// .serve(router)
|
||||
/// .await;
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
|
||||
|
||||
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use salvo::prelude::*;
|
||||
/// use std::{net::TcpListener, sync::Arc};
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let router = Router::new()
|
||||
/// .connect_hot_reload();
|
||||
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
/// .serve(router)
|
||||
/// .await;
|
||||
/// }
|
||||
fn connect_hot_reload(self) -> Self;
|
||||
|
||||
/// Serves the static WASM for your Dioxus application (except the generated index.html).
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use salvo::prelude::*;
|
||||
/// use std::{net::TcpListener, sync::Arc};
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let router = Router::new()
|
||||
/// .server_static_assets("/dist");
|
||||
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
/// .serve(router)
|
||||
/// .await;
|
||||
/// }
|
||||
/// ```
|
||||
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
|
||||
|
||||
/// Serves the Dioxus application. This will serve a complete server side rendered application.
|
||||
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// #![allow(non_snake_case)]
|
||||
/// use dioxus_lib::prelude::*;
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
/// use salvo::prelude::*;
|
||||
/// use std::{net::TcpListener, sync::Arc};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let router = Router::new().serve_dioxus_application("", ServerConfig::new(app, ()));
|
||||
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
|
||||
/// .serve(router)
|
||||
/// .await;
|
||||
/// }
|
||||
///
|
||||
/// fn app() -> Element {unimplemented!() }
|
||||
/// ```
|
||||
fn serve_dioxus_application(
|
||||
self,
|
||||
server_fn_path: &'static str,
|
||||
cfg: impl Into<ServeConfig>,
|
||||
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
impl DioxusRouterExt for Router {
|
||||
fn register_server_fns_with_handler<H>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
|
||||
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
|
||||
let full_route = format!("{server_fn_route}/{server_fn_path}");
|
||||
match func.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => {
|
||||
router = router.push(Router::with_path(&full_route).post(handler(func)));
|
||||
}
|
||||
Encoding::GetJSON | Encoding::GetCBOR => {
|
||||
router = router.push(Router::with_path(&full_route).get(handler(func)));
|
||||
}
|
||||
}
|
||||
}
|
||||
router
|
||||
}
|
||||
|
||||
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
|
||||
self.register_server_fns_with_handler(server_fn_route, |func| ServerFnHandler {
|
||||
server_context: DioxusServerContext::default(),
|
||||
function: func,
|
||||
})
|
||||
}
|
||||
|
||||
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
|
||||
let assets_path = assets_path.into();
|
||||
|
||||
// Serve all files in dist folder except index.html
|
||||
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Couldn't read assets directory at {:?}: {}",
|
||||
&assets_path, e
|
||||
)
|
||||
});
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
if path.ends_with("index.html") {
|
||||
continue;
|
||||
}
|
||||
let route = path
|
||||
.strip_prefix(&assets_path)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
segment.to_str().unwrap_or_else(|| {
|
||||
panic!("Failed to convert path segment {:?} to string", segment)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
if path.is_file() {
|
||||
let route = format!("/{}", route);
|
||||
let serve_dir = StaticFile::new(path.clone());
|
||||
self = self.push(Router::with_path(route).get(serve_dir))
|
||||
} else {
|
||||
let route = format!("/{}/<**path>", route);
|
||||
let serve_dir = StaticDir::new([path.clone()]);
|
||||
self = self.push(Router::with_path(route).get(serve_dir))
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn serve_dioxus_application(
|
||||
self,
|
||||
server_fn_path: &'static str,
|
||||
cfg: impl Into<ServeConfig>,
|
||||
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let cfg = cfg.into();
|
||||
|
||||
self.serve_static_assets(cfg.assets_path.clone())
|
||||
.connect_hot_reload()
|
||||
.register_server_fns(server_fn_path)
|
||||
.push(Router::with_path("/<**any_path>").get(SSRHandler {
|
||||
config: cfg,
|
||||
virtual_dom: virtual_dom_factory,
|
||||
}))
|
||||
}
|
||||
|
||||
fn connect_hot_reload(self) -> Self {
|
||||
let mut _dioxus_router = Router::with_path("_dioxus");
|
||||
_dioxus_router =
|
||||
_dioxus_router.push(Router::with_path("hot_reload").handle(HotReloadHandler));
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
{
|
||||
_dioxus_router = _dioxus_router.push(Router::with_path("disconnect").handle(ignore_ws));
|
||||
}
|
||||
self.push(_dioxus_router)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the parts of a request that are needed for server functions. This will take parts of the request and replace them with empty values.
|
||||
pub fn extract_parts(req: &mut Request) -> http::request::Parts {
|
||||
let mut parts = http::request::Request::new(()).into_parts().0;
|
||||
parts.method = std::mem::take(req.method_mut());
|
||||
parts.uri = std::mem::take(req.uri_mut());
|
||||
parts.version = req.version();
|
||||
parts.headers = std::mem::take(req.headers_mut());
|
||||
parts.extensions = std::mem::take(req.extensions_mut());
|
||||
|
||||
parts
|
||||
}
|
||||
|
||||
fn apply_request_parts_to_response(
|
||||
headers: hyper::header::HeaderMap,
|
||||
response: &mut salvo::prelude::Response,
|
||||
) {
|
||||
let mut_headers = response.headers_mut();
|
||||
for (key, value) in headers.iter() {
|
||||
mut_headers.insert(key, value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn convert_request(req: &mut Request) -> Result<HyperRequest, SalvoError> {
|
||||
let forward_url: hyper::Uri = TryFrom::try_from(req.uri()).map_err(SalvoError::other)?;
|
||||
let mut build = hyper::Request::builder()
|
||||
.method(req.method())
|
||||
.uri(&forward_url);
|
||||
for (key, value) in req.headers() {
|
||||
build = build.header(key, value);
|
||||
}
|
||||
static SECURE_MAX_SIZE: usize = 64 * 1024;
|
||||
|
||||
let body = Limited::new(req.take_body(), SECURE_MAX_SIZE)
|
||||
.collect()
|
||||
.await
|
||||
.map_err(ParseError::other)?
|
||||
.to_bytes();
|
||||
build.body(body.into()).map_err(SalvoError::other)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn convert_response(response: HyperResponse, res: &mut Response) {
|
||||
let (parts, body) = response.into_parts();
|
||||
let http::response::Parts {
|
||||
version,
|
||||
headers,
|
||||
status,
|
||||
..
|
||||
} = parts;
|
||||
res.status_code = Some(status);
|
||||
res.version = version;
|
||||
res.cookies = CookieJar::new();
|
||||
for cookie in headers.get_all(http::header::SET_COOKIE).iter() {
|
||||
if let Some(cookie) = cookie
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|s| Cookie::parse(s.to_string()).ok())
|
||||
{
|
||||
res.cookies.add_original(cookie);
|
||||
}
|
||||
}
|
||||
res.headers = headers;
|
||||
res.version = version;
|
||||
if let Ok(bytes) = hyper::body::to_bytes(body).await {
|
||||
res.body = bytes.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler that renders a Dioxus application to HTML using server-side rendering.
|
||||
pub struct SSRHandler {
|
||||
config: ServeConfig,
|
||||
virtual_dom: Box<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||
}
|
||||
|
||||
impl SSRHandler {
|
||||
/// Creates a new SSR handler with the given configuration.
|
||||
pub fn new(
|
||||
config: ServeConfig,
|
||||
virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
virtual_dom: Box::new(virtual_dom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Handler for SSRHandler {
|
||||
async fn handle(
|
||||
&self,
|
||||
req: &mut Request,
|
||||
depot: &mut Depot,
|
||||
res: &mut Response,
|
||||
_flow: &mut FlowCtrl,
|
||||
) {
|
||||
// Get the SSR renderer from the depot or create a new one if it doesn't exist
|
||||
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
|
||||
renderer.clone()
|
||||
} else {
|
||||
let renderer = SSRState::new(&self.cfg);
|
||||
depot.inject(renderer.clone());
|
||||
renderer
|
||||
};
|
||||
|
||||
let route = req.uri().path().to_string();
|
||||
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(extract_parts(req)));
|
||||
let server_context = DioxusServerContext::new(parts);
|
||||
|
||||
match renderer_pool
|
||||
.render(route, &self.cfg, &server_context)
|
||||
.await
|
||||
{
|
||||
Ok(rendered) => {
|
||||
let crate::render::RenderResponse { html, freshness } = rendered;
|
||||
|
||||
res.write_body(html).unwrap();
|
||||
|
||||
let headers = server_context.response_parts().unwrap().headers.clone();
|
||||
apply_request_parts_to_response(headers, res);
|
||||
freshness.write(res.headers_mut());
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error rendering SSR: {}", err);
|
||||
res.write_body("Error rendering SSR").unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
|
||||
pub struct ServerFnHandler {
|
||||
server_context: DioxusServerContext,
|
||||
function: server_fn::ServerFnTraitObj<()>,
|
||||
}
|
||||
|
||||
impl ServerFnHandler {
|
||||
/// Create a new server function handler with the given server context and server function.
|
||||
pub fn new(
|
||||
server_context: impl Into<DioxusServerContext>,
|
||||
function: server_fn::ServerFnTraitObj<()>,
|
||||
) -> Self {
|
||||
let server_context = server_context.into();
|
||||
Self {
|
||||
server_context,
|
||||
function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
impl ServerFnHandler {
|
||||
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
|
||||
match convert_request(req).await {
|
||||
Ok(hyper_req) => {
|
||||
let response =
|
||||
server_fn_service(self.server_context.clone(), self.function.clone())
|
||||
.run(hyper_req)
|
||||
.await
|
||||
.unwrap();
|
||||
convert_response(response, res).await;
|
||||
}
|
||||
Err(err) => handle_error(err, res),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_error(error: impl Error + Send + Sync, res: &mut Response) {
|
||||
let mut resp_err = Response::new();
|
||||
resp_err.status_code(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
resp_err.render(format!("Internal Server Error: {}", error));
|
||||
*res = resp_err;
|
||||
}
|
||||
|
||||
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
|
||||
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
|
||||
#[derive(Default)]
|
||||
pub struct HotReloadHandler;
|
||||
|
||||
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
|
||||
#[handler]
|
||||
impl HotReloadHandler {
|
||||
async fn handle(
|
||||
&self,
|
||||
_req: &mut Request,
|
||||
_depot: &mut Depot,
|
||||
_res: &mut Response,
|
||||
) -> Result<(), salvo::http::StatusError> {
|
||||
Err(salvo::http::StatusError::not_found())
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
#[derive(Default)]
|
||||
pub struct HotReloadHandler;
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
#[handler]
|
||||
impl HotReloadHandler {
|
||||
async fn handle(
|
||||
&self,
|
||||
req: &mut Request,
|
||||
_depot: &mut Depot,
|
||||
res: &mut Response,
|
||||
) -> Result<(), salvo::http::StatusError> {
|
||||
use salvo::websocket::Message;
|
||||
use salvo::websocket::WebSocketUpgrade;
|
||||
|
||||
let state = crate::hot_reload::spawn_hot_reload().await;
|
||||
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, move |mut websocket| async move {
|
||||
use futures_util::StreamExt;
|
||||
|
||||
println!("🔥 Hot Reload WebSocket connected");
|
||||
{
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
{
|
||||
println!("🔮 Finding updates since last compile...");
|
||||
let templates_read = state.templates.read().await;
|
||||
|
||||
for template in &*templates_read {
|
||||
if websocket
|
||||
.send(Message::text(serde_json::to_string(&template).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("finished");
|
||||
}
|
||||
|
||||
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
|
||||
state.message_receiver.clone(),
|
||||
);
|
||||
while let Some(change) = rx.next().await {
|
||||
if let Some(template) = change {
|
||||
let template = { serde_json::to_string(&template).unwrap() };
|
||||
if websocket.send(Message::text(template)).await.is_err() {
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
#[handler]
|
||||
async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> {
|
||||
use salvo::websocket::WebSocketUpgrade;
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, |mut ws| async move {
|
||||
let _ = ws.send(salvo::websocket::Message::text("connected")).await;
|
||||
while let Some(msg) = ws.recv().await {
|
||||
if msg.is_err() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
//! Dioxus utilities for the [Warp](https://docs.rs/warp/latest/warp/index.html) server framework.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust
|
||||
//! #![allow(non_snake_case)]
|
||||
//! use dioxus_lib::prelude::*;
|
||||
//! use dioxus_fullstack::prelude::*;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! #[cfg(feature = "web")]
|
||||
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
||||
//! #[cfg(feature = "server")]
|
||||
//! {
|
||||
//! tokio::runtime::Runtime::new()
|
||||
//! .unwrap()
|
||||
//! .block_on(async move {
|
||||
//! let routes = serve_dioxus_application("", ServerConfig::new(app, ()));
|
||||
//! warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn app() -> Element {
|
||||
//! let text = use_signal(|| "...".to_string());
|
||||
//!
|
||||
//! rsx! {
|
||||
//! button {
|
||||
//! onclick: move |_| {
|
||||
//! to_owned![text];
|
||||
//! async move {
|
||||
//! if let Ok(data) = get_server_data().await {
|
||||
//! text.set(data);
|
||||
//! }
|
||||
//! }
|
||||
//! },
|
||||
//! "Run a server function"
|
||||
//! }
|
||||
//! "Server said: {text}"
|
||||
//! })
|
||||
//! }
|
||||
//!
|
||||
//! #[server(GetServerData)]
|
||||
//! async fn get_server_data() -> Result<String, ServerFnError> {
|
||||
//! Ok("Hello from the server!".to_string())
|
||||
//! }
|
||||
//!
|
||||
//! ```
|
||||
|
||||
use crate::layer::Service;
|
||||
use crate::{
|
||||
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
|
||||
};
|
||||
|
||||
use crate::server_fn_service;
|
||||
use dioxus_lib::prelude::VirtualDom;
|
||||
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::task::spawn_blocking;
|
||||
use warp::path::FullPath;
|
||||
use warp::Rejection;
|
||||
use warp::{
|
||||
filters::BoxedFilter,
|
||||
http::{Response, StatusCode},
|
||||
hyper::body::Bytes,
|
||||
path, Filter, Reply,
|
||||
};
|
||||
|
||||
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use warp::{body, header, hyper::HeaderMap, path, post, Filter};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let routes = register_server_fns_with_handler(server_fn_route, |full_route, func| {
|
||||
/// path(full_route)
|
||||
/// .and(warp::post().or(warp::get()).unify())
|
||||
/// .and(request_parts())
|
||||
/// .and(warp::body::bytes())
|
||||
/// .and_then(move |parts, bytes: bytes::Bytes| {
|
||||
/// let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
|
||||
/// async move {
|
||||
/// let req = warp::hyper::Request::from_parts(parts, bytes.into());
|
||||
/// service.run(req).await.map_err(|err| {
|
||||
/// tracing::error!("Server function error: {}", err);
|
||||
/// warp::reject::reject()
|
||||
/// })
|
||||
/// }
|
||||
/// })
|
||||
/// })
|
||||
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn register_server_fns_with_handler<H, F, R>(
|
||||
server_fn_route: &'static str,
|
||||
mut handler: H,
|
||||
) -> BoxedFilter<(R,)>
|
||||
where
|
||||
H: FnMut(String, server_fn::ServerFnTraitObj<()>) -> F,
|
||||
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
|
||||
F::Extract: Send,
|
||||
R: Reply + 'static,
|
||||
{
|
||||
let mut filter: Option<BoxedFilter<F::Extract>> = None;
|
||||
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
|
||||
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
|
||||
let full_route = format!("{server_fn_route}/{server_fn_path}")
|
||||
.trim_start_matches('/')
|
||||
.to_string();
|
||||
let route = handler(full_route, func).boxed();
|
||||
if let Some(boxed_filter) = filter.take() {
|
||||
filter = Some(boxed_filter.or(route).unify().boxed());
|
||||
} else {
|
||||
filter = Some(route);
|
||||
}
|
||||
}
|
||||
filter.expect("No server functions found")
|
||||
}
|
||||
|
||||
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let routes = register_server_fns("");
|
||||
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
|
||||
register_server_fns_with_handler(server_fn_route, |full_route, func| {
|
||||
path(full_route)
|
||||
.and(warp::post().or(warp::get()).unify())
|
||||
.and(request_parts())
|
||||
.and(warp::body::bytes())
|
||||
.and_then(move |parts, bytes: bytes::Bytes| {
|
||||
let mut service = server_fn_service(DioxusServerContext::default(), func.clone());
|
||||
async move {
|
||||
let req = warp::hyper::Request::from_parts(parts, bytes.into());
|
||||
service.run(req).await.map_err(|err| {
|
||||
tracing::error!("Server function error: {}", err);
|
||||
|
||||
struct WarpServerFnError(String);
|
||||
impl std::fmt::Debug for WarpServerFnError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl warp::reject::Reject for WarpServerFnError {}
|
||||
|
||||
warp::reject::custom(WarpServerFnError(err.to_string()))
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Serves the Dioxus application. This will serve a complete server side rendered application.
|
||||
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// #![allow(non_snake_case)]
|
||||
/// use dioxus_lib::prelude::*;
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let routes = serve_dioxus_application("", ServerConfig::new(app, ()));
|
||||
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
/// }
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// None
|
||||
/// }
|
||||
/// ```
|
||||
pub fn serve_dioxus_application(
|
||||
server_fn_route: &'static str,
|
||||
cfg: impl Into<ServeConfig>,
|
||||
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> BoxedFilter<(impl Reply,)> {
|
||||
let cfg = cfg.into();
|
||||
// Serve the dist folder and the index.html file
|
||||
let serve_dir = warp::fs::dir(cfg.assets_path.clone());
|
||||
|
||||
let virtual_dom_factory =
|
||||
Arc::new(virtual_dom_factory) as Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>;
|
||||
|
||||
connect_hot_reload()
|
||||
// First register the server functions
|
||||
.or(register_server_fns(server_fn_route))
|
||||
// Then the index route
|
||||
.or(path::end().and(render_ssr(cfg.clone(), {
|
||||
let virtual_dom_factory = virtual_dom_factory.clone();
|
||||
move || virtual_dom_factory()
|
||||
})))
|
||||
// Then the static assets
|
||||
.or(serve_dir)
|
||||
// Then all other routes
|
||||
.or(render_ssr(cfg, move || virtual_dom_factory()))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
/// Server render the application.
|
||||
pub fn render_ssr(
|
||||
cfg: ServeConfig,
|
||||
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
|
||||
warp::get()
|
||||
.and(request_parts())
|
||||
.and(with_ssr_state(&cfg, virtual_dom_factory))
|
||||
.then(
|
||||
move |parts: http::request::Parts,
|
||||
(renderer, virtual_dom_factory): (
|
||||
SSRState,
|
||||
Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>,
|
||||
)| {
|
||||
let route = parts.uri.path().to_string();
|
||||
let parts = Arc::new(RwLock::new(parts));
|
||||
let cfg = cfg.clone();
|
||||
async move {
|
||||
let server_context = DioxusServerContext::new(parts);
|
||||
|
||||
match renderer
|
||||
.render(route, &cfg, move || virtual_dom_factory(), &server_context)
|
||||
.await
|
||||
{
|
||||
Ok(rendered) => {
|
||||
let crate::render::RenderResponse { html, freshness } = rendered;
|
||||
|
||||
let mut res = Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(html)
|
||||
.unwrap();
|
||||
|
||||
let headers_mut = res.headers_mut();
|
||||
let headers = server_context.response_parts().unwrap().headers.clone();
|
||||
for (key, value) in headers.iter() {
|
||||
headers_mut.insert(key, value.clone());
|
||||
}
|
||||
freshness.write(headers_mut);
|
||||
|
||||
res
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to render ssr: {}", err);
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body("Failed to render ssr".into())
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request.
|
||||
pub fn request_parts(
|
||||
) -> impl Filter<Extract = (http::request::Parts,), Error = warp::reject::Rejection> + Clone {
|
||||
warp::method()
|
||||
.and(warp::filters::path::full())
|
||||
.and(
|
||||
warp::filters::query::raw()
|
||||
.or(warp::any().map(String::new))
|
||||
.unify(),
|
||||
)
|
||||
.and(warp::header::headers_cloned())
|
||||
.and_then(move |method, path: FullPath, query, headers| async move {
|
||||
http::uri::Builder::new()
|
||||
.path_and_query(format!("{}?{}", path.as_str(), query))
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
|
||||
})
|
||||
.map(|uri| {
|
||||
let mut req = http::Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.body(())
|
||||
.unwrap();
|
||||
req.headers_mut().extend(headers);
|
||||
req.into_parts().0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn with_ssr_state(
|
||||
cfg: &ServeConfig,
|
||||
virtual_dom_factory: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> impl Filter<
|
||||
Extract = ((
|
||||
SSRState,
|
||||
Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>,
|
||||
),),
|
||||
Error = std::convert::Infallible,
|
||||
> + Clone {
|
||||
let renderer = SSRState::new(cfg);
|
||||
let virtual_dom_factory =
|
||||
Arc::new(virtual_dom_factory) as Arc<dyn Fn() -> VirtualDom + Send + Sync + 'static>;
|
||||
warp::any().map(move || (renderer.clone(), virtual_dom_factory.clone()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FailedToReadBody(String);
|
||||
|
||||
impl warp::reject::Reject for FailedToReadBody {}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecieveFailed(String);
|
||||
|
||||
impl warp::reject::Reject for RecieveFailed {}
|
||||
|
||||
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// #![allow(non_snake_case)]
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let routes = connect_hot_reload();
|
||||
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
|
||||
{
|
||||
#[cfg(not(all(debug_assertions, feature = "hot-reload")))]
|
||||
{
|
||||
warp::path!("_dioxus" / "hot_reload")
|
||||
.map(warp::reply)
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND))
|
||||
}
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
{
|
||||
use crate::hot_reload::HotReloadState;
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use warp::ws::Message;
|
||||
|
||||
let hot_reload = warp::path!("_dioxus" / "hot_reload")
|
||||
.and(warp::any().then(crate::hot_reload::spawn_hot_reload))
|
||||
.and(warp::ws())
|
||||
.map(move |state: &'static HotReloadState, ws: warp::ws::Ws| {
|
||||
// #[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||
ws.on_upgrade(move |mut websocket| {
|
||||
async move {
|
||||
println!("🔥 Hot Reload WebSocket connected");
|
||||
{
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
{
|
||||
println!("🔮 Finding updates since last compile...");
|
||||
let templates_read = state.templates.read().await;
|
||||
|
||||
for template in &*templates_read {
|
||||
if websocket
|
||||
.send(Message::text(
|
||||
serde_json::to_string(&template).unwrap(),
|
||||
))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("finished");
|
||||
}
|
||||
|
||||
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
|
||||
state.message_receiver.clone(),
|
||||
);
|
||||
while let Some(change) = rx.next().await {
|
||||
if let Some(template) = change {
|
||||
let template = { serde_json::to_string(&template).unwrap() };
|
||||
if websocket.send(Message::text(template)).await.is_err() {
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
let disconnect =
|
||||
warp::path!("_dioxus" / "disconnect")
|
||||
.and(warp::ws())
|
||||
.map(move |ws: warp::ws::Ws| {
|
||||
println!("disconnect");
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload",))]
|
||||
ws.on_upgrade(move |mut websocket| async move {
|
||||
struct DisconnectOnDrop(Option<warp::ws::WebSocket>);
|
||||
impl Drop for DisconnectOnDrop {
|
||||
fn drop(&mut self) {
|
||||
std::mem::drop(self.0.take().unwrap().close());
|
||||
}
|
||||
}
|
||||
|
||||
let _ = websocket.send(Message::text("connected")).await;
|
||||
let mut ws = DisconnectOnDrop(Some(websocket));
|
||||
|
||||
loop {
|
||||
if ws.0.as_mut().unwrap().next().await.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
disconnect.or(hot_reload)
|
||||
}
|
||||
}
|
|
@ -54,64 +54,26 @@
|
|||
//! }
|
||||
//! ```
|
||||
|
||||
use axum::routing::*;
|
||||
use axum::{
|
||||
body::{self, Body, BoxBody},
|
||||
body::{self, Body},
|
||||
extract::State,
|
||||
handler::Handler,
|
||||
http::{Request, Response, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use dioxus_lib::prelude::VirtualDom;
|
||||
use server_fn::{Encoding, ServerFunctionRegistry};
|
||||
use http::header::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext,
|
||||
server_fn::DioxusServerFnRegistry,
|
||||
};
|
||||
|
||||
/// A extension trait with utilities for integrating Dioxus with your Axum router.
|
||||
pub trait DioxusRouterExt<S> {
|
||||
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use dioxus_lib::prelude::*;
|
||||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
/// axum::Server::bind(&addr)
|
||||
/// .serve(
|
||||
/// axum::Router::new()
|
||||
/// .register_server_fns_with_handler("", |func| {
|
||||
/// move |req: Request<Body>| async move {
|
||||
/// let (parts, body) = req.into_parts();
|
||||
/// let parts: Arc<http::request::Parts> = Arc::new(parts.into());
|
||||
/// let server_context = DioxusServerContext::new(parts.clone());
|
||||
/// server_fn_handler(server_context, func.clone(), parts, body).await
|
||||
/// }
|
||||
/// })
|
||||
/// .into_make_service(),
|
||||
/// )
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
fn register_server_fns_with_handler<H, T>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler<T, S>,
|
||||
T: 'static,
|
||||
S: Clone + Send + Sync + 'static;
|
||||
|
||||
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -133,7 +95,7 @@ pub trait DioxusRouterExt<S> {
|
|||
/// .unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
|
||||
fn register_server_fns(self) -> Self;
|
||||
|
||||
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
|
||||
///
|
||||
|
@ -218,7 +180,6 @@ pub trait DioxusRouterExt<S> {
|
|||
/// ```
|
||||
fn serve_dioxus_application(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
cfg: impl Into<ServeConfig>,
|
||||
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self;
|
||||
|
@ -228,51 +189,21 @@ impl<S> DioxusRouterExt<S> for Router<S>
|
|||
where
|
||||
S: Send + Sync + Clone + 'static,
|
||||
{
|
||||
fn register_server_fns_with_handler<H, T>(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
mut handler: impl FnMut(server_fn::ServerFnTraitObj<()>) -> H,
|
||||
) -> Self
|
||||
where
|
||||
H: Handler<T, S, Body>,
|
||||
T: 'static,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
|
||||
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
|
||||
let full_route = format!("{server_fn_route}/{server_fn_path}");
|
||||
match func.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => {
|
||||
router = router.route(&full_route, post(handler(func)));
|
||||
}
|
||||
Encoding::GetJSON | Encoding::GetCBOR => {
|
||||
router = router.route(&full_route, get(handler(func)));
|
||||
}
|
||||
}
|
||||
}
|
||||
router
|
||||
}
|
||||
fn register_server_fns(mut self) -> Self {
|
||||
use http::method::Method;
|
||||
|
||||
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
|
||||
self.register_server_fns_with_handler(server_fn_route, |func| {
|
||||
move |req: Request<Body>| {
|
||||
let mut service = crate::server_fn_service(Default::default(), func);
|
||||
async move {
|
||||
let (req, body) = req.into_parts();
|
||||
let req = Request::from_parts(req, body);
|
||||
let res = service.run(req);
|
||||
match res.await {
|
||||
Ok(res) => Ok::<_, std::convert::Infallible>(res.map(|b| b.into())),
|
||||
Err(e) => {
|
||||
let mut res = Response::new(Body::from(e.to_string()));
|
||||
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
for (path, method) in server_fn::axum::server_fn_paths() {
|
||||
tracing::trace!("Registering server function: {} {}", method, path);
|
||||
let handler = move |req| handle_server_fns_inner(path, || {}, req);
|
||||
self = match method {
|
||||
Method::GET => self.route(path, get(handler)),
|
||||
Method::POST => self.route(path, post(handler)),
|
||||
Method::PUT => self.route(path, put(handler)),
|
||||
_ => todo!(),
|
||||
};
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
|
||||
|
@ -317,7 +248,6 @@ where
|
|||
|
||||
fn serve_dioxus_application(
|
||||
self,
|
||||
server_fn_route: &'static str,
|
||||
cfg: impl Into<ServeConfig>,
|
||||
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
|
@ -327,7 +257,7 @@ where
|
|||
// Add server functions and render index.html
|
||||
self.serve_static_assets(cfg.assets_path.clone())
|
||||
.connect_hot_reload()
|
||||
.register_server_fns(server_fn_route)
|
||||
.register_server_fns()
|
||||
.fallback(get(render_handler).with_state((cfg, Arc::new(build_virtual_dom), ssr_state)))
|
||||
}
|
||||
|
||||
|
@ -371,6 +301,13 @@ fn apply_request_parts_to_response<B>(
|
|||
}
|
||||
}
|
||||
|
||||
type AxumHandler<F> = (
|
||||
F,
|
||||
ServeConfig,
|
||||
SSRState,
|
||||
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||
);
|
||||
|
||||
/// SSR renderer handler for Axum with added context injection.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -419,17 +356,13 @@ fn apply_request_parts_to_response<B>(
|
|||
/// }
|
||||
/// ```
|
||||
pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
|
||||
State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<(
|
||||
F,
|
||||
ServeConfig,
|
||||
SSRState,
|
||||
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||
)>,
|
||||
State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<AxumHandler<F>>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
let (parts, _) = request.into_parts();
|
||||
let url = parts.uri.path_and_query().unwrap().to_string();
|
||||
let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
|
||||
let parts: Arc<tokio::sync::RwLock<http::request::Parts>> =
|
||||
Arc::new(tokio::sync::RwLock::new(parts));
|
||||
let mut server_context = DioxusServerContext::new(parts.clone());
|
||||
inject_context(&mut server_context);
|
||||
|
||||
|
@ -452,13 +385,15 @@ pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
|
|||
}
|
||||
}
|
||||
|
||||
type RenderHandlerExtractor = (
|
||||
ServeConfig,
|
||||
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||
SSRState,
|
||||
);
|
||||
|
||||
/// SSR renderer handler for Axum
|
||||
pub async fn render_handler(
|
||||
State((cfg, virtual_dom_factory, ssr_state)): State<(
|
||||
ServeConfig,
|
||||
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||
SSRState,
|
||||
)>,
|
||||
State((cfg, virtual_dom_factory, ssr_state)): State<RenderHandlerExtractor>,
|
||||
request: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
render_handler_with_context(
|
||||
|
@ -468,10 +403,10 @@ pub async fn render_handler(
|
|||
.await
|
||||
}
|
||||
|
||||
fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
|
||||
fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(body::boxed(format!("Error: {}", e)))
|
||||
.body(body::Body::new(format!("Error: {}", e)))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
|
@ -516,3 +451,98 @@ pub async fn hot_reload_handler(ws: axum::extract::WebSocketUpgrade) -> impl Int
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A handler for Dioxus server functions. This will run the server function and return the result.
|
||||
async fn handle_server_fns_inner(
|
||||
path: &str,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
use server_fn::middleware::Service;
|
||||
|
||||
let path_string = path.to_string();
|
||||
|
||||
let future = move || async move {
|
||||
let (parts, body) = req.into_parts();
|
||||
let req = Request::from_parts(parts.clone(), body);
|
||||
|
||||
if let Some(mut service) =
|
||||
server_fn::axum::get_server_fn_service(&path_string)
|
||||
{
|
||||
|
||||
let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
|
||||
additional_context();
|
||||
|
||||
// store Accepts and Referrer in case we need them for redirect (below)
|
||||
let accepts_html = req
|
||||
.headers()
|
||||
.get(ACCEPT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.contains("text/html"))
|
||||
.unwrap_or(false);
|
||||
let referrer = req.headers().get(REFERER).cloned();
|
||||
|
||||
// actually run the server fn
|
||||
let mut res = service.run(req).await;
|
||||
|
||||
|
||||
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
||||
// Location set, then redirect to to Referer
|
||||
if accepts_html {
|
||||
if let Some(referrer) = referrer {
|
||||
let has_location = res.headers().get(LOCATION).is_some();
|
||||
if !has_location {
|
||||
*res.status_mut() = StatusCode::FOUND;
|
||||
res.headers_mut().insert(LOCATION, referrer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply the response parts from the server context to the response
|
||||
let mut res_options = server_context.response_parts_mut().unwrap();
|
||||
res.headers_mut().extend(res_options.headers.drain());
|
||||
|
||||
Ok(res)
|
||||
} else {
|
||||
Response::builder().status(StatusCode::BAD_REQUEST).body(
|
||||
{
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
Body::from(format!(
|
||||
"No server function found for path: {path_string}\nYou may need to explicitly register the server function with `register_explicit`, rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
|
||||
))
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
Body::from(format!(
|
||||
"No server function found for path: {path_string}\nYou may need to rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.",
|
||||
))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.expect("could not build Response")
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
use futures_util::future::FutureExt;
|
||||
|
||||
let result = tokio::task::spawn_local(future);
|
||||
let result = result.then(|f| async move { f.unwrap() });
|
||||
result.await.unwrap_or_else(|e| {
|
||||
use server_fn::error::NoCustomError;
|
||||
use server_fn::error::ServerFnErrorSerde;
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ServerFnError::<NoCustomError>::ServerError(e.to_string())
|
||||
.ser()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.into_response()
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
future().await
|
||||
}
|
||||
}
|
|
@ -53,7 +53,7 @@ impl Config {
|
|||
|
||||
/// Set the address to serve the app on.
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub fn addr(self, addr: impl Into<std::net::SocketAddr>) -> Self {
|
||||
let addr = addr.into();
|
||||
Self { addr, ..self }
|
||||
|
@ -61,7 +61,7 @@ impl Config {
|
|||
|
||||
/// Set the route to the server functions.
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub fn server_fn_route(self, server_fn_route: &'static str) -> Self {
|
||||
Self {
|
||||
server_fn_route,
|
||||
|
@ -71,7 +71,7 @@ impl Config {
|
|||
|
||||
/// Set the incremental renderer config.
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub fn incremental(self, cfg: IncrementalRendererConfig) -> Self {
|
||||
Self {
|
||||
server_cfg: self.server_cfg.incremental(cfg),
|
||||
|
@ -81,21 +81,21 @@ impl Config {
|
|||
|
||||
/// Set the server config.
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub fn server_cfg(self, server_cfg: ServeConfigBuilder) -> Self {
|
||||
Self { server_cfg, ..self }
|
||||
}
|
||||
|
||||
/// Set the web config.
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "web")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "web")))]
|
||||
pub fn web_cfg(self, web_cfg: dioxus_web::Config) -> Self {
|
||||
Self { web_cfg, ..self }
|
||||
}
|
||||
|
||||
/// Set the desktop config.
|
||||
#[cfg(feature = "desktop")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "desktop")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "desktop")))]
|
||||
pub fn desktop_cfg(self, desktop_cfg: dioxus_desktop::Config) -> Self {
|
||||
Self {
|
||||
desktop_cfg,
|
||||
|
@ -105,13 +105,13 @@ impl Config {
|
|||
|
||||
/// Set the mobile config.
|
||||
#[cfg(feature = "mobile")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "mobile")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
|
||||
pub fn mobile_cfg(self, mobile_cfg: dioxus_mobile::Config) -> Self {
|
||||
Self { mobile_cfg, ..self }
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// Launch a server application
|
||||
pub async fn launch_server(
|
||||
self,
|
||||
|
@ -121,14 +121,15 @@ impl Config {
|
|||
println!("Listening on {}", addr);
|
||||
let cfg = self.server_cfg.build();
|
||||
let server_fn_route = self.server_fn_route;
|
||||
#[cfg(all(feature = "axum", not(feature = "warp"), not(feature = "salvo")))]
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
{
|
||||
use crate::adapters::axum_adapter::{render_handler, DioxusRouterExt};
|
||||
use crate::axum_adapter::{render_handler, DioxusRouterExt};
|
||||
use axum::routing::get;
|
||||
use tower::ServiceBuilder;
|
||||
|
||||
let ssr_state = SSRState::new(&cfg);
|
||||
let router = axum::Router::new().register_server_fns(server_fn_route);
|
||||
let router = axum::Router::new().register_server_fns();
|
||||
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
|
||||
let router = router
|
||||
.serve_static_assets(cfg.assets_path.clone())
|
||||
|
@ -144,52 +145,8 @@ impl Config {
|
|||
.layer(tower_http::compression::CompressionLayer::new().gzip(true)),
|
||||
)
|
||||
.into_make_service();
|
||||
axum::Server::bind(&addr).serve(router).await.unwrap();
|
||||
}
|
||||
#[cfg(all(feature = "warp", not(feature = "axum"), not(feature = "salvo")))]
|
||||
{
|
||||
use warp::Filter;
|
||||
// First register the server functions
|
||||
let router = register_server_fns(server_fn_route);
|
||||
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
|
||||
let router = {
|
||||
// Serve the dist folder and the index.html file
|
||||
let serve_dir = warp::fs::dir(cfg.assets_path);
|
||||
let build_virtual_dom = Arc::new(build_virtual_dom);
|
||||
|
||||
router
|
||||
.or(connect_hot_reload())
|
||||
// Then the index route
|
||||
.or(warp::path::end().and(render_ssr(cfg.clone(), {
|
||||
let build_virtual_dom = build_virtual_dom.clone();
|
||||
move || build_virtual_dom()
|
||||
})))
|
||||
// Then the static assets
|
||||
.or(serve_dir)
|
||||
// Then all other routes
|
||||
.or(render_ssr(cfg, move || build_virtual_dom()))
|
||||
};
|
||||
warp::serve(router.boxed().with(warp::filters::compression::gzip()))
|
||||
.run(addr)
|
||||
.await;
|
||||
}
|
||||
#[cfg(all(feature = "salvo", not(feature = "axum"), not(feature = "warp")))]
|
||||
{
|
||||
use crate::adapters::salvo_adapter::{DioxusRouterExt, SSRHandler};
|
||||
use salvo::conn::Listener;
|
||||
let router = salvo::Router::new().register_server_fns(server_fn_route);
|
||||
#[cfg(not(any(feature = "desktop", feature = "mobile")))]
|
||||
let router = router
|
||||
.serve_static_assets(cfg.assets_path)
|
||||
.connect_hot_reload()
|
||||
.push(salvo::Router::with_path("/<**any_path>").get(SSRHandler::new(cfg)));
|
||||
let router = router.hoop(
|
||||
salvo::compression::Compression::new()
|
||||
.enable_gzip(salvo::prelude::CompressionLevel::Default),
|
||||
);
|
||||
salvo::Server::new(salvo::conn::tcp::TcpListener::new(addr).bind().await)
|
||||
.serve(router)
|
||||
.await;
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, router).await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use dioxus_lib::prelude::{Element, VirtualDom};
|
|||
pub use crate::Config;
|
||||
|
||||
/// Launch a fullstack app with the given root component, contexts, and config.
|
||||
#[allow(unused)]
|
||||
pub fn launch(
|
||||
root: fn() -> Element,
|
||||
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
use std::pin::Pin;
|
||||
use tracing_futures::Instrument;
|
||||
|
||||
use http::{Request, Response};
|
||||
|
||||
/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service
|
||||
pub trait Layer: Send + Sync + 'static {
|
||||
/// Wrap a boxed service with this layer
|
||||
fn layer(&self, inner: BoxedService) -> BoxedService;
|
||||
}
|
||||
|
||||
impl<L> Layer for L
|
||||
where
|
||||
L: tower_layer::Layer<BoxedService> + Sync + Send + 'static,
|
||||
L::Service: Service + Send + 'static,
|
||||
{
|
||||
fn layer(&self, inner: BoxedService) -> BoxedService {
|
||||
BoxedService(Box::new(self.layer(inner)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A service is a function that takes a request and returns an async response
|
||||
pub trait Service {
|
||||
/// Run the service and produce a future that resolves to a response
|
||||
fn run(
|
||||
&mut self,
|
||||
req: http::Request<hyper::body::Body>,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
|
||||
> + Send,
|
||||
>,
|
||||
>;
|
||||
}
|
||||
|
||||
impl<S> Service for S
|
||||
where
|
||||
S: tower::Service<http::Request<hyper::body::Body>, Response = Response<hyper::body::Body>>,
|
||||
S::Future: Send + 'static,
|
||||
S::Error: Into<server_fn::ServerFnError>,
|
||||
{
|
||||
fn run(
|
||||
&mut self,
|
||||
req: http::Request<hyper::body::Body>,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = Result<Response<hyper::body::Body>, server_fn::ServerFnError>,
|
||||
> + Send,
|
||||
>,
|
||||
> {
|
||||
let fut = self.call(req).instrument(tracing::trace_span!(
|
||||
"service",
|
||||
"{}",
|
||||
std::any::type_name::<S>()
|
||||
));
|
||||
Box::pin(async move { fut.await.map_err(|err| err.into()) })
|
||||
}
|
||||
}
|
||||
|
||||
/// A boxed service is a type-erased service that can be used without knowing the underlying type
|
||||
pub struct BoxedService(pub Box<dyn Service + Send>);
|
||||
|
||||
impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {
|
||||
type Response = http::Response<hyper::body::Body>;
|
||||
type Error = server_fn::ServerFnError;
|
||||
type Future = Pin<
|
||||
Box<
|
||||
dyn std::future::Future<
|
||||
Output = Result<http::Response<hyper::body::Body>, server_fn::ServerFnError>,
|
||||
> + Send,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
Ok(()).into()
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<hyper::body::Body>) -> Self::Future {
|
||||
self.0.run(req)
|
||||
}
|
||||
}
|
|
@ -2,125 +2,91 @@
|
|||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(any(docsrs, feature = "nightly-doc"), feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub use once_cell;
|
||||
|
||||
mod html_storage;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod adapters;
|
||||
// Splitting up the glob export lets us document features required for each adapter
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "axum")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||
#[cfg(feature = "axum")]
|
||||
pub use adapters::axum_adapter;
|
||||
// TODO: Compilation seems to be broken with the salvo feature enabled. Fix and add more features to checks in CI
|
||||
// #[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "salvo")))]
|
||||
// #[cfg(feature = "salvo")]
|
||||
// pub use adapters::salvo_adapter;
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "warp")))]
|
||||
#[cfg(feature = "warp")]
|
||||
pub use adapters::warp_adapter;
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg(feature = "server")]
|
||||
pub use adapters::{server_fn_service, ServerFnHandler};
|
||||
mod axum_adapter;
|
||||
|
||||
mod config;
|
||||
mod hooks;
|
||||
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "server"))]
|
||||
mod hot_reload;
|
||||
pub mod launch;
|
||||
|
||||
#[cfg(all(
|
||||
debug_assertions,
|
||||
feature = "hot-reload",
|
||||
feature = "server",
|
||||
not(target_arch = "wasm32")
|
||||
))]
|
||||
mod hot_reload;
|
||||
pub use config::*;
|
||||
#[cfg(feature = "server")]
|
||||
mod layer;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod render;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod serve_config;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
mod server_context;
|
||||
mod server_fn;
|
||||
|
||||
/// A prelude of commonly used items in dioxus-fullstack.
|
||||
pub mod prelude {
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "axum")))]
|
||||
pub use crate::adapters::axum_adapter::*;
|
||||
// #[cfg(feature = "salvo")]
|
||||
// #[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "salvo")))]
|
||||
// pub use crate::adapters::salvo_adapter::*;
|
||||
#[cfg(feature = "warp")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "warp")))]
|
||||
pub use crate::adapters::warp_adapter::*;
|
||||
use crate::hooks;
|
||||
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||
pub use crate::axum_adapter::*;
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
#[cfg_attr(
|
||||
any(docsrs, feature = "nightly-doc"),
|
||||
doc(cfg(not(feature = "server")))
|
||||
)]
|
||||
#[cfg_attr(docsrs, doc(cfg(not(feature = "server"))))]
|
||||
pub use crate::html_storage::deserialize::get_root_props_from_document;
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
pub use crate::layer::{Layer, Service};
|
||||
|
||||
#[cfg(all(feature = "server", feature = "router"))]
|
||||
#[cfg_attr(
|
||||
any(docsrs, feature = "nightly-doc"),
|
||||
doc(cfg(all(feature = "server", feature = "router")))
|
||||
)]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "router"))))]
|
||||
pub use crate::render::pre_cache_static_routes_with_props;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub use crate::render::SSRState;
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "router")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
||||
pub use crate::router::FullstackRouterConfig;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
|
||||
|
||||
#[cfg(all(feature = "server", feature = "axum"))]
|
||||
#[cfg_attr(
|
||||
any(docsrs, feature = "nightly-doc"),
|
||||
doc(cfg(all(feature = "server", feature = "axum")))
|
||||
)]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "server", feature = "axum"))))]
|
||||
pub use crate::server_context::Axum;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub use crate::server_context::{
|
||||
extract, server_context, DioxusServerContext, FromServerContext, ProvideServerContext,
|
||||
};
|
||||
pub use crate::server_fn::DioxusServerFn;
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
pub use crate::server_fn::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction};
|
||||
pub use dioxus_server_macro::*;
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
|
||||
pub use server_fn::{self, ServerFn as _, ServerFnError};
|
||||
|
||||
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub use dioxus_ssr::incremental::IncrementalRendererConfig;
|
||||
|
||||
pub use dioxus_server_macro::*;
|
||||
pub use server_fn::{self, ServerFn as _, ServerFnError};
|
||||
}
|
||||
|
||||
// Warn users about overlapping features
|
||||
#[cfg(all(
|
||||
feature = "server",
|
||||
feature = "web",
|
||||
not(doc),
|
||||
not(feature = "nightly-doc")
|
||||
))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
|
||||
// // Warn users about overlapping features
|
||||
// #[cfg(all(feature = "server", feature = "web", not(doc)))]
|
||||
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "server",
|
||||
feature = "desktop",
|
||||
not(doc),
|
||||
not(feature = "nightly-doc")
|
||||
))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
|
||||
// #[cfg(all(feature = "server", feature = "desktop", not(doc)))]
|
||||
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "server",
|
||||
feature = "mobile",
|
||||
not(doc),
|
||||
not(feature = "nightly-doc")
|
||||
))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `mobile` feature are overlapping. Please choose one or the other.");
|
||||
// #[cfg(all(feature = "server", feature = "mobile", not(doc)))]
|
||||
// compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `mobile` feature are overlapping. Please choose one or the other.");
|
||||
|
|
|
@ -6,11 +6,10 @@ use dioxus_ssr::{
|
|||
incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody},
|
||||
Renderer,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::task::{spawn_blocking, JoinHandle};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::prelude::*;
|
||||
use dioxus_lib::prelude::*;
|
||||
|
@ -22,7 +21,7 @@ where
|
|||
{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
spawn_blocking(move || {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on(f())
|
||||
|
@ -48,7 +47,6 @@ impl SsrRendererPool {
|
|||
server_context: &DioxusServerContext,
|
||||
) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
let wrapper = FullstackRenderer {
|
||||
serialized_props: None,
|
||||
cfg: cfg.clone(),
|
||||
server_context: server_context.clone(),
|
||||
};
|
||||
|
@ -66,7 +64,7 @@ impl SsrRendererPool {
|
|||
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.rebuild(&mut NoOpMutations);
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
|
@ -91,7 +89,11 @@ impl SsrRendererPool {
|
|||
let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
|
||||
}
|
||||
Err(err) => {
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err));
|
||||
_ = tx.send(Err(
|
||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
|
||||
err,
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -122,7 +124,7 @@ impl SsrRendererPool {
|
|||
.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.rebuild(&mut NoOpMutations);
|
||||
vdom.wait_for_suspense().await;
|
||||
tracing::info!("Suspense resolved");
|
||||
// after polling the future, we need to restore the context
|
||||
|
@ -192,31 +194,25 @@ impl SSRState {
|
|||
}
|
||||
|
||||
/// Render the application to HTML.
|
||||
pub fn render<'a>(
|
||||
pub async fn render<'a>(
|
||||
&'a self,
|
||||
route: String,
|
||||
cfg: &'a ServeConfig,
|
||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||
server_context: &'a DioxusServerContext,
|
||||
) -> impl std::future::Future<
|
||||
Output = Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError>,
|
||||
> + Send
|
||||
+ 'a {
|
||||
async move {
|
||||
let ServeConfig { .. } = cfg;
|
||||
) -> Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
let ServeConfig { .. } = cfg;
|
||||
|
||||
let (freshness, html) = self
|
||||
.renderers
|
||||
.render_to(cfg, route, virtual_dom_factory, server_context)
|
||||
.await?;
|
||||
let (freshness, html) = self
|
||||
.renderers
|
||||
.render_to(cfg, route, virtual_dom_factory, server_context)
|
||||
.await?;
|
||||
|
||||
Ok(RenderResponse { html, freshness })
|
||||
}
|
||||
Ok(RenderResponse { html, freshness })
|
||||
}
|
||||
}
|
||||
|
||||
struct FullstackRenderer {
|
||||
serialized_props: Option<String>,
|
||||
cfg: ServeConfig,
|
||||
server_context: DioxusServerContext,
|
||||
}
|
||||
|
@ -330,7 +326,7 @@ impl RenderResponse {
|
|||
fn pre_renderer() -> Renderer {
|
||||
let mut renderer = Renderer::default();
|
||||
renderer.pre_render = true;
|
||||
renderer.into()
|
||||
renderer
|
||||
}
|
||||
|
||||
fn incremental_pre_renderer(
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
#![allow(non_snake_case)]
|
||||
//! Configeration for how to serve a Dioxus application
|
||||
//! Configuration for how to serve a Dioxus application
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
use crate::router::*;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use dioxus_lib::prelude::*;
|
||||
|
||||
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ServeConfigBuilder {
|
||||
pub(crate) root_id: Option<&'static str>,
|
||||
pub(crate) index_html: Option<String>,
|
||||
|
@ -149,6 +145,7 @@ pub(crate) struct IndexHtml {
|
|||
#[derive(Clone)]
|
||||
pub struct ServeConfig {
|
||||
pub(crate) index: IndexHtml,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) assets_path: PathBuf,
|
||||
pub(crate) incremental:
|
||||
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::html_storage::HTMLData;
|
||||
pub use server_fn_impl::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
|
@ -13,7 +12,7 @@ pub struct DioxusServerContext {
|
|||
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
|
||||
>,
|
||||
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
|
||||
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
|
||||
pub(crate) parts: Arc<tokio::sync::RwLock<http::request::Parts>>,
|
||||
html_data: Arc<RwLock<HTMLData>>,
|
||||
}
|
||||
|
||||
|
@ -25,7 +24,9 @@ impl Default for DioxusServerContext {
|
|||
response_parts: std::sync::Arc::new(RwLock::new(
|
||||
http::response::Response::new(()).into_parts().0,
|
||||
)),
|
||||
parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
|
||||
parts: std::sync::Arc::new(tokio::sync::RwLock::new(
|
||||
http::request::Request::new(()).into_parts().0,
|
||||
)),
|
||||
html_data: Arc::new(RwLock::new(HTMLData::default())),
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +42,7 @@ mod server_fn_impl {
|
|||
|
||||
impl DioxusServerContext {
|
||||
/// Create a new server context from a request
|
||||
pub fn new(parts: impl Into<Arc<RwLock<http::request::Parts>>>) -> Self {
|
||||
pub fn new(parts: impl Into<Arc<tokio::sync::RwLock<http::request::Parts>>>) -> Self {
|
||||
Self {
|
||||
parts: parts.into(),
|
||||
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
|
||||
|
@ -83,19 +84,15 @@ mod server_fn_impl {
|
|||
/// Get the request that triggered:
|
||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||
/// - The server function to be called if called from a server function after the initial render
|
||||
pub fn request_parts(
|
||||
&self,
|
||||
) -> std::sync::LockResult<RwLockReadGuard<'_, http::request::Parts>> {
|
||||
self.parts.read()
|
||||
pub fn request_parts(&self) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
|
||||
self.parts.blocking_read()
|
||||
}
|
||||
|
||||
/// Get the request that triggered:
|
||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||
/// - The server function to be called if called from a server function after the initial render
|
||||
pub fn request_parts_mut(
|
||||
&self,
|
||||
) -> std::sync::LockResult<RwLockWriteGuard<'_, http::request::Parts>> {
|
||||
self.parts.write()
|
||||
pub fn request_parts_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
|
||||
self.parts.blocking_write()
|
||||
}
|
||||
|
||||
/// Extract some part from the request
|
||||
|
@ -123,7 +120,7 @@ mod server_fn_impl {
|
|||
}
|
||||
|
||||
std::thread_local! {
|
||||
pub(crate) static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() ));
|
||||
pub(crate) static SERVER_CONTEXT: std::cell::RefCell<Box<DioxusServerContext>> = Default::default();
|
||||
}
|
||||
|
||||
/// Get information about the current server request.
|
||||
|
@ -186,7 +183,7 @@ impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
|
|||
}
|
||||
|
||||
/// A trait for extracting types from the server context
|
||||
#[async_trait::async_trait(?Send)]
|
||||
#[async_trait::async_trait]
|
||||
pub trait FromServerContext<I = ()>: Sized {
|
||||
/// The error type returned when extraction fails. This type must implement `std::error::Error`.
|
||||
type Rejection: std::error::Error;
|
||||
|
@ -216,24 +213,24 @@ impl<T: 'static> std::error::Error for NotFoundInServerContext<T> {}
|
|||
|
||||
pub struct FromContext<T: std::marker::Send + std::marker::Sync + Clone + 'static>(pub(crate) T);
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
#[async_trait::async_trait]
|
||||
impl<T: Send + Sync + Clone + 'static> FromServerContext for FromContext<T> {
|
||||
type Rejection = NotFoundInServerContext<T>;
|
||||
|
||||
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
|
||||
Ok(Self(req.clone().get::<T>().ok_or_else(|| {
|
||||
Ok(Self(req.clone().get::<T>().ok_or({
|
||||
NotFoundInServerContext::<T>(std::marker::PhantomData::<T>)
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "axum")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||
/// An adapter for axum extractors for the server context
|
||||
pub struct Axum;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[async_trait::async_trait(?Send)]
|
||||
#[async_trait::async_trait]
|
||||
impl<
|
||||
I: axum::extract::FromRequestParts<(), Rejection = R>,
|
||||
R: axum::response::IntoResponse + std::error::Error,
|
||||
|
@ -242,6 +239,6 @@ impl<
|
|||
type Rejection = R;
|
||||
|
||||
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
|
||||
Ok(I::from_request_parts(&mut *req.request_parts_mut().unwrap(), &()).await?)
|
||||
Ok(I::from_request_parts(&mut req.request_parts_mut(), &()).await?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
#[derive(Clone)]
|
||||
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
|
||||
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<()>);
|
||||
|
@ -41,7 +41,7 @@ impl ServerFnTraitObj {
|
|||
server_fn::inventory::collect!(ServerFnTraitObj);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// Middleware for a server function
|
||||
pub struct ServerFnMiddleware {
|
||||
/// The prefix of the server function.
|
||||
|
@ -75,7 +75,7 @@ pub(crate) static MIDDLEWARE: once_cell::sync::Lazy<
|
|||
server_fn::inventory::collect!(ServerFnMiddleware);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// A server function that can be called on serializable arguments and returns a serializable result.
|
||||
pub type ServerFunction = server_fn::SerializedFnTraitObj<()>;
|
||||
|
||||
|
@ -92,7 +92,7 @@ static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
|
|||
});
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// The registry of all Dioxus server functions.
|
||||
pub struct DioxusServerFnRegistry;
|
||||
|
||||
|
@ -160,7 +160,7 @@ impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
|
|||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
/// Errors that can occur when registering a server function.
|
||||
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ServerRegistrationFnError {
|
||||
|
@ -171,23 +171,3 @@ pub enum ServerRegistrationFnError {
|
|||
#[error("The server function registry is poisoned: {0}")]
|
||||
Poisoned(String),
|
||||
}
|
||||
|
||||
/// Defines a "server function." A server function can be called from the server or the client,
|
||||
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
|
||||
///
|
||||
/// Server functions are created using the `server` macro.
|
||||
///
|
||||
/// The set of server functions
|
||||
/// can be queried on the server for routing purposes by calling [server_fn::ServerFunctionRegistry::get].
|
||||
///
|
||||
/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself.
|
||||
pub trait DioxusServerFn: server_fn::ServerFn<()> {
|
||||
/// Registers the server function, allowing the client to query it by URL.
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(any(docsrs, feature = "nightly-doc"), doc(cfg(feature = "server")))]
|
||||
fn register_explicit() -> Result<(), server_fn::ServerFnError> {
|
||||
Self::register_in_explicit::<DioxusServerFnRegistry>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<()> {}
|
|
@ -1,19 +1,17 @@
|
|||
use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::HotReloadMsg;
|
||||
use dioxus_core::Template;
|
||||
use dioxus_rsx::{
|
||||
hot_reload::{FileMap, FileMapBuildResult, UpdateResult},
|
||||
HotReloadingContext,
|
||||
};
|
||||
use interprocess_docfix::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use interprocess_docfix::local_socket::LocalSocketListener;
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "file_watcher")]
|
||||
use dioxus_html::HtmlCtx;
|
||||
|
|
5
packages/liveview/.vscode/settings.json
vendored
5
packages/liveview/.vscode/settings.json
vendored
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"rust-analyzer.cargo.features": [
|
||||
"warp"
|
||||
]
|
||||
}
|
|
@ -31,42 +31,20 @@ dioxus-hot-reload = { workspace = true, optional = true }
|
|||
dioxus-cli-config = { workspace = true }
|
||||
generational-box = { workspace = true }
|
||||
|
||||
# warp
|
||||
warp = { version = "0.3.3", optional = true }
|
||||
|
||||
# axum
|
||||
axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
||||
|
||||
# salvo
|
||||
salvo = { version = "0.63.0", optional = true, features = ["websocket", "affix"] }
|
||||
once_cell = "1.17.1"
|
||||
async-trait = "0.1.71"
|
||||
|
||||
# rocket
|
||||
rocket = { version = "0.5.0", optional = true }
|
||||
rocket_ws = { version = "0.1.0", optional = true }
|
||||
|
||||
# actix is ... complicated?
|
||||
# actix-files = { version = "0.6.2", optional = true }
|
||||
# actix-web = { version = "4.2.1", optional = true }
|
||||
# actix-ws = { version = "0.2.5", optional = true }
|
||||
axum = { workspace = true, optional = true, features = ["ws"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_env_logger = { version = "0.5.0" }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
warp = "0.3.3"
|
||||
axum = { version = "0.6.1", features = ["ws"] }
|
||||
salvo = { version = "0.63.0", features = ["affix", "websocket"] }
|
||||
rocket = "0.5.0"
|
||||
rocket_ws = "0.1.0"
|
||||
tower = "0.4.13"
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
tower = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["hot-reload"]
|
||||
# actix = ["actix-files", "actix-web", "actix-ws"]
|
||||
axum = ["dep:axum"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
rocket = ["dep:rocket", "dep:rocket_ws"]
|
||||
|
||||
[[example]]
|
||||
name = "axum"
|
||||
|
@ -75,15 +53,3 @@ required-features = ["axum"]
|
|||
[[example]]
|
||||
name = "axum_stress"
|
||||
required-features = ["axum"]
|
||||
|
||||
[[example]]
|
||||
name = "salvo"
|
||||
required-features = ["salvo"]
|
||||
|
||||
[[example]]
|
||||
name = "warp"
|
||||
required-features = ["warp"]
|
||||
|
||||
[[example]]
|
||||
name = "rocket"
|
||||
required-features = ["rocket"]
|
||||
|
|
|
@ -26,9 +26,6 @@
|
|||
The current backend frameworks supported include:
|
||||
|
||||
- Axum
|
||||
- Warp
|
||||
- Salvo
|
||||
- Rocket
|
||||
|
||||
Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework.
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ async fn main() {
|
|||
|
||||
println!("Listening on http://{addr}");
|
||||
|
||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -55,8 +55,8 @@ async fn main() {
|
|||
|
||||
println!("Listening on http://{addr}");
|
||||
|
||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_liveview::LiveViewPool;
|
||||
use rocket::response::content::RawHtml;
|
||||
use rocket::{Config, Rocket, State};
|
||||
use rocket_ws::{Channel, WebSocket};
|
||||
|
||||
fn app() -> Element {
|
||||
let mut num = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
"hello Rocket! {num}"
|
||||
button { onclick: move |_| num += 1, "Increment" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn index_page_with_glue(glue: &str) -> RawHtml<String> {
|
||||
RawHtml(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>Dioxus LiveView with Rocket</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
glue = glue
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(config: &Config) -> RawHtml<String> {
|
||||
index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!(
|
||||
"ws://{addr}:{port}/ws",
|
||||
addr = config.address,
|
||||
port = config.port,
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/as-path")]
|
||||
async fn as_path() -> RawHtml<String> {
|
||||
index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws"))
|
||||
}
|
||||
|
||||
#[get("/ws")]
|
||||
fn ws(ws: WebSocket, pool: &State<LiveViewPool>) -> Channel<'static> {
|
||||
let pool = pool.inner().to_owned();
|
||||
|
||||
ws.channel(move |stream| {
|
||||
Box::pin(async move {
|
||||
let _ = pool
|
||||
.launch(dioxus_liveview::rocket_socket(stream), app)
|
||||
.await;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let view = dioxus_liveview::LiveViewPool::new();
|
||||
|
||||
Rocket::build()
|
||||
.manage(view)
|
||||
.mount("/", routes![index, as_path, ws])
|
||||
.ignite()
|
||||
.await
|
||||
.expect("Failed to ignite rocket")
|
||||
.launch()
|
||||
.await
|
||||
.expect("Failed to launch rocket");
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_liveview::LiveViewPool;
|
||||
use salvo::affix;
|
||||
use salvo::prelude::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn app() -> Element {
|
||||
let mut num = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
"hello salvo! {num}"
|
||||
button { onclick: move |_| num += 1, "Increment" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let addr = "127.0.0.1:3030";
|
||||
let acceptor = TcpListener::new(addr).bind().await;
|
||||
|
||||
let view = LiveViewPool::new();
|
||||
|
||||
let router = Router::new()
|
||||
.hoop(affix::inject(Arc::new(view)))
|
||||
.get(index)
|
||||
.push(Router::with_path("ws").get(connect));
|
||||
|
||||
println!("Listening on http://{}", addr);
|
||||
|
||||
Server::new(acceptor).serve(router).await;
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(res: &mut Response) {
|
||||
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||
res.render(Text::Html(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>Dioxus LiveView with Salvo</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
|
||||
)));
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn connect(
|
||||
req: &mut Request,
|
||||
depot: &mut Depot,
|
||||
res: &mut Response,
|
||||
) -> Result<(), StatusError> {
|
||||
let view = depot.obtain::<Arc<LiveViewPool>>().unwrap().clone();
|
||||
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, |ws| async move {
|
||||
_ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
|
||||
})
|
||||
.await
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_liveview::warp_adapter::warp_socket;
|
||||
use dioxus_liveview::LiveViewPool;
|
||||
use std::net::SocketAddr;
|
||||
use warp::ws::Ws;
|
||||
use warp::Filter;
|
||||
|
||||
fn app() -> Element {
|
||||
let mut num = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
"hello warp! {num}"
|
||||
button {
|
||||
onclick: move |_| num += 1,
|
||||
"Increment"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||
|
||||
let index = warp::path::end().map(move || {
|
||||
warp::reply::html(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>Dioxus LiveView with Warp</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/"))
|
||||
))
|
||||
});
|
||||
|
||||
let pool = LiveViewPool::new();
|
||||
|
||||
let ws = warp::path("ws")
|
||||
.and(warp::ws())
|
||||
.and(warp::any().map(move || pool.clone()))
|
||||
.map(move |ws: Ws, pool: LiveViewPool| {
|
||||
ws.on_upgrade(|ws| async move {
|
||||
let _ = pool.launch(warp_socket(ws), app).await;
|
||||
})
|
||||
});
|
||||
|
||||
println!("Listening on http://{}", addr);
|
||||
|
||||
warp::serve(index.or(ws)).run(addr).await;
|
||||
}
|
|
@ -81,10 +81,8 @@ impl LiveviewRouter for Router {
|
|||
}
|
||||
|
||||
async fn start(self, address: impl Into<std::net::SocketAddr>) {
|
||||
if let Err(err) = axum::Server::bind(&address.into())
|
||||
.serve(self.into_make_service())
|
||||
.await
|
||||
{
|
||||
let listener = tokio::net::TcpListener::bind(address.into()).await.unwrap();
|
||||
if let Err(err) = axum::serve(listener, self.into_make_service()).await {
|
||||
eprintln!("Failed to start axum server: {}", err);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,26 +2,11 @@ use std::future::Future;
|
|||
|
||||
use dioxus_core::{Element, VirtualDom};
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
pub mod warp_adapter;
|
||||
#[cfg(feature = "warp")]
|
||||
pub use warp_adapter::*;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum_adapter;
|
||||
#[cfg(feature = "axum")]
|
||||
pub use axum_adapter::*;
|
||||
|
||||
#[cfg(feature = "salvo")]
|
||||
pub mod salvo_adapter;
|
||||
#[cfg(feature = "salvo")]
|
||||
pub use salvo_adapter::*;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket_adapter;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub use rocket_adapter::*;
|
||||
|
||||
/// A trait for servers that can be used to host a LiveView app.
|
||||
pub trait LiveviewRouter {
|
||||
/// Create a new router.
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
use crate::LiveViewPool;
|
||||
use crate::LiveviewRouter;
|
||||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use rocket::futures::{SinkExt, StreamExt};
|
||||
use rocket::response::content::RawHtml;
|
||||
use rocket::{get, routes, State};
|
||||
use rocket_ws::Channel;
|
||||
use rocket_ws::WebSocket;
|
||||
use rocket_ws::{result::Error, stream::DuplexStream, Message};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Convert a Rocket WebSocket into a `LiveViewSocket`.
|
||||
///
|
||||
/// This is required to launch a LiveView app using the Rocket web framework.
|
||||
pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
|
||||
stream
|
||||
.map(transform_rx)
|
||||
.with(transform_tx)
|
||||
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
|
||||
message
|
||||
.map_err(|_| LiveViewError::SendingFailed)?
|
||||
.into_text()
|
||||
.map(|s| s.into_bytes())
|
||||
.map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
|
||||
Ok(Message::Binary(message))
|
||||
}
|
||||
|
||||
impl LiveviewRouter for rocket::Rocket<rocket::Build> {
|
||||
fn create_default_liveview_router() -> Self {
|
||||
Self::build()
|
||||
}
|
||||
|
||||
fn with_virtual_dom(
|
||||
self,
|
||||
route: &str,
|
||||
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
#[get("/")]
|
||||
async fn index(request: &rocket::route::Route) -> RawHtml<String> {
|
||||
let route = request.uri.base();
|
||||
|
||||
let glue = crate::interpreter_glue(&format!("{route}/ws",));
|
||||
|
||||
let title = crate::app_title();
|
||||
|
||||
RawHtml(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>{title}</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/ws")]
|
||||
fn ws(ws: WebSocket, app: &State<LiveviewApp>) -> Channel<'static> {
|
||||
let app = app.inner();
|
||||
let pool = app.pool.clone();
|
||||
let app = app.app.clone();
|
||||
|
||||
ws.channel(move |stream| {
|
||||
Box::pin(async move {
|
||||
let _ = pool
|
||||
.launch_virtualdom(crate::rocket_socket(stream), move || app())
|
||||
.await;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
struct LiveviewApp {
|
||||
app: Arc<dyn Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static>,
|
||||
pool: LiveViewPool,
|
||||
}
|
||||
|
||||
let app = Arc::new(app);
|
||||
|
||||
let view = crate::LiveViewPool::new();
|
||||
|
||||
self.manage(LiveviewApp {
|
||||
app: app,
|
||||
pool: view,
|
||||
})
|
||||
.mount(route, routes![index, ws])
|
||||
}
|
||||
|
||||
async fn start(self, address: impl Into<std::net::SocketAddr>) {
|
||||
let address = address.into();
|
||||
|
||||
let figment = self
|
||||
.figment()
|
||||
.clone()
|
||||
.merge((rocket::Config::PORT, address.port()))
|
||||
.merge((rocket::Config::ADDRESS, address.ip()));
|
||||
|
||||
self.configure(figment)
|
||||
.ignite()
|
||||
.await
|
||||
.expect("Failed to ignite rocket")
|
||||
.launch()
|
||||
.await
|
||||
.expect("Failed to launch rocket");
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
use crate::LiveViewPool;
|
||||
use crate::LiveviewRouter;
|
||||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use salvo::conn::TcpListener;
|
||||
use salvo::http::StatusError;
|
||||
use salvo::websocket::WebSocketUpgrade;
|
||||
use salvo::websocket::{Message, WebSocket};
|
||||
use salvo::writing::Text;
|
||||
use salvo::Listener;
|
||||
use salvo::Server;
|
||||
use salvo::{handler, Depot, Request, Response, Router};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Convert a Salvo WebSocket into a `LiveViewSocket`.
|
||||
///
|
||||
/// This is required to launch a LiveView app using the Salvo web framework.
|
||||
pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
|
||||
ws.map(transform_rx)
|
||||
.with(transform_tx)
|
||||
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
fn transform_rx(message: Result<Message, salvo::Error>) -> Result<Vec<u8>, LiveViewError> {
|
||||
let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
|
||||
|
||||
Ok(as_bytes.into())
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, salvo::Error> {
|
||||
Ok(Message::binary(message))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LiveviewApp {
|
||||
app: Arc<dyn Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static>,
|
||||
pool: Arc<LiveViewPool>,
|
||||
}
|
||||
|
||||
impl LiveviewRouter for Router {
|
||||
fn create_default_liveview_router() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
||||
fn with_virtual_dom(
|
||||
self,
|
||||
route: &str,
|
||||
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let app = Arc::new(app);
|
||||
|
||||
let view = crate::LiveViewPool::new();
|
||||
|
||||
self.push(
|
||||
Router::with_path(route)
|
||||
.hoop(salvo::affix::inject(LiveviewApp {
|
||||
app: app,
|
||||
pool: Arc::new(view),
|
||||
}))
|
||||
.get(index)
|
||||
.push(Router::with_path("ws").get(connect)),
|
||||
)
|
||||
}
|
||||
|
||||
async fn start(self, address: impl Into<std::net::SocketAddr>) {
|
||||
let address = address.into();
|
||||
|
||||
let acceptor = TcpListener::new(address).bind().await;
|
||||
Server::new(acceptor).serve(self).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(req: &mut Request, res: &mut Response) {
|
||||
let base = req.uri().path();
|
||||
let title = crate::app_title();
|
||||
|
||||
res.render(Text::Html(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>{title}</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
glue = crate::interpreter_glue(&format!("{base}/ws"))
|
||||
)));
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn connect(
|
||||
req: &mut Request,
|
||||
depot: &mut Depot,
|
||||
res: &mut Response,
|
||||
) -> Result<(), StatusError> {
|
||||
let app = depot.obtain::<LiveviewApp>().unwrap().clone();
|
||||
let view = app.pool.clone();
|
||||
let app = app.app.clone();
|
||||
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, |ws| async move {
|
||||
_ = view
|
||||
.launch_virtualdom(crate::salvo_socket(ws), move || app())
|
||||
.await;
|
||||
})
|
||||
.await
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use warp::ws::{Message, WebSocket};
|
||||
|
||||
/// Convert a warp WebSocket into a `LiveViewSocket`.
|
||||
///
|
||||
/// This is required to launch a LiveView app using the warp web framework.
|
||||
pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
|
||||
ws.map(transform_rx)
|
||||
.with(transform_tx)
|
||||
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
fn transform_rx(message: Result<Message, warp::Error>) -> Result<Vec<u8>, LiveViewError> {
|
||||
// Destructure the `message` into the buffer we got from warp.
|
||||
let msg = message
|
||||
.map_err(|_| LiveViewError::SendingFailed)?
|
||||
.into_bytes();
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, warp::Error> {
|
||||
Ok(Message::binary(message))
|
||||
}
|
|
@ -1,12 +1,7 @@
|
|||
use dioxus_core::*;
|
||||
use std::any::Any;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub type Config = crate::Config<axum::Router>;
|
||||
#[cfg(all(feature = "salvo", not(feature = "axum")))]
|
||||
pub type Config = crate::Config<salvo::Router>;
|
||||
#[cfg(all(feature = "rocket", not(any(feature = "axum", feature = "salvo"))))]
|
||||
pub type Config = crate::Config<rocket::Rocket<rocket::Build>>;
|
||||
|
||||
/// Launches the WebView and runs the event loop, with configuration and root props.
|
||||
pub fn launch(
|
||||
|
|
|
@ -15,6 +15,7 @@ mod config;
|
|||
mod eval;
|
||||
mod events;
|
||||
pub use config::*;
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod launch;
|
||||
|
||||
pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
|
||||
|
@ -25,7 +26,7 @@ impl<T> WebsocketRx for T where T: StreamExt<Item = Result<String, LiveViewError
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LiveViewError {
|
||||
#[error("warp error")]
|
||||
#[error("Sending to client error")]
|
||||
SendingFailed,
|
||||
}
|
||||
|
||||
|
@ -113,7 +114,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
|
|||
// If the url starts with a `/`, generate glue which reuses current host
|
||||
let get_ws_url = if url_or_path.starts_with('/') {
|
||||
r#"
|
||||
let loc = window.location;
|
||||
let loc = window.location;
|
||||
let new_url = "";
|
||||
if (loc.protocol === "https:") {{
|
||||
new_url = "wss:";
|
||||
|
@ -135,7 +136,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
|
|||
function __dioxusGetWsUrl(path) {{
|
||||
{get_ws_url}
|
||||
}}
|
||||
|
||||
|
||||
var WS_ADDR = __dioxusGetWsUrl("{url_or_path}");
|
||||
{handle_edits}
|
||||
</script>
|
||||
|
|
|
@ -10,4 +10,4 @@ publish = false
|
|||
dioxus = { workspace = true }
|
||||
dioxus-liveview = { workspace = true, features = ["axum"] }
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
axum = { version = "0.6.1", features = ["ws"] }
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
|
|
|
@ -57,8 +57,8 @@ async fn main() {
|
|||
|
||||
println!("Listening on http://{addr}");
|
||||
|
||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
|
|||
fullstack = ["dioxus-fullstack"]
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { version = "0.6.1", features = ["ws"] }
|
||||
axum = { workspace = true, features = ["ws"] }
|
||||
dioxus = { workspace = true, features = ["router" ]}
|
||||
# dioxus-liveview = { workspace = true, features = ["axum"] }
|
||||
dioxus-ssr = { path = "../ssr" }
|
||||
|
|
|
@ -11,7 +11,45 @@ use dioxus_ssr::Renderer;
|
|||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("build 1000 routes", |b| {
|
||||
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
|
||||
let mut renderer = IncrementalRenderer::builder()
|
||||
.static_dir("./static")
|
||||
.invalidate_after(Duration::from_secs(10))
|
||||
.build();
|
||||
let wrapper = DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
</head>
|
||||
<body>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
for id in 0..1000 {
|
||||
render_route(
|
||||
&mut renderer,
|
||||
Route::Post { id },
|
||||
&mut tokio::io::sink(),
|
||||
|_| Box::pin(async move {}),
|
||||
&wrapper,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("build 1000 routes no memory cache", |b| {
|
||||
let wrapper = DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
@ -25,54 +63,22 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
})
|
||||
.static_dir("./static")
|
||||
.invalidate_after(Duration::from_secs(10))
|
||||
.build();
|
||||
|
||||
b.iter(|| {
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
for id in 0..1000 {
|
||||
render_route(
|
||||
&mut renderer,
|
||||
Route::Post { id },
|
||||
&mut tokio::io::sink(),
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
c.bench_function("build 1000 routes no memory cache", |b| {
|
||||
};
|
||||
b.to_async(tokio::runtime::Runtime::new().unwrap())
|
||||
.iter(|| async {
|
||||
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
</head>
|
||||
<body>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
})
|
||||
.static_dir("./static")
|
||||
.memory_cache_limit(0)
|
||||
.invalidate_after(Duration::from_secs(10))
|
||||
.build();
|
||||
let mut renderer = IncrementalRenderer::builder()
|
||||
.static_dir("./static")
|
||||
.memory_cache_limit(0)
|
||||
.invalidate_after(Duration::from_secs(10))
|
||||
.build();
|
||||
|
||||
for id in 0..1000 {
|
||||
render_route(
|
||||
&mut renderer,
|
||||
Route::Post { id },
|
||||
&mut tokio::io::sink(),
|
||||
|_| {},
|
||||
|_| Box::pin(async move {}),
|
||||
&wrapper,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -89,7 +95,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
RenderPathProps::builder().path(Route::Post { id }).build(),
|
||||
);
|
||||
|
||||
vdom.rebuild();
|
||||
vdom.rebuild_in_place();
|
||||
|
||||
struct Ignore;
|
||||
|
||||
|
@ -104,27 +110,28 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
|||
})
|
||||
});
|
||||
c.bench_function("cache static", |b| {
|
||||
let wrapper = DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
</head>
|
||||
<body>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
};
|
||||
b.to_async(tokio::runtime::Runtime::new().unwrap())
|
||||
.iter(|| async {
|
||||
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
|
||||
before_body: r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,
|
||||
initial-scale=1.0">
|
||||
<title>Dioxus Application</title>
|
||||
</head>
|
||||
<body>"#
|
||||
.to_string(),
|
||||
after_body: r#"</body>
|
||||
</html>"#
|
||||
.to_string(),
|
||||
})
|
||||
.static_dir("./static")
|
||||
.build();
|
||||
let mut renderer = IncrementalRenderer::builder()
|
||||
.static_dir("./static")
|
||||
.build();
|
||||
|
||||
pre_cache_static_routes::<Route, _>(&mut renderer)
|
||||
pre_cache_static_routes::<Route, _>(&mut renderer, &wrapper)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
|
@ -146,7 +153,7 @@ fn Blog() -> Element {
|
|||
#[component]
|
||||
fn Post(id: usize) -> Element {
|
||||
rsx! {
|
||||
for _ in 0..*id {
|
||||
for _ in 0..id {
|
||||
div {
|
||||
"PostId: {id}"
|
||||
}
|
||||
|
|
|
@ -26,28 +26,31 @@ async fn main() {
|
|||
"/ws",
|
||||
get(move |ws: WebSocketUpgrade| async move {
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
_ = view
|
||||
.launch(dioxus_liveview::axum_socket(socket), Route::Home {})
|
||||
.await;
|
||||
_ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
println!("Listening on http://{listen_address}");
|
||||
|
||||
axum::Server::bind(&listen_address.to_string().parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
let listener = tokio::net::TcpListener::bind(&listen_address)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "liveview"))]
|
||||
fn main() {
|
||||
launch(|| {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
})
|
||||
launch(app)
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
|
@ -166,8 +166,8 @@ where
|
|||
///
|
||||
/// Panics if the function is not called in a dioxus runtime with a Liveview context.
|
||||
pub fn new_with_initial_path(initial_path: R) -> Self {
|
||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
|
||||
let action_rx = Arc::new(Mutex::new(action_rx));
|
||||
let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
|
||||
|
||||
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
|
||||
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
||||
Arc::new(RwLock::new(Arc::new(|| {})));
|
||||
|
@ -175,20 +175,17 @@ where
|
|||
let eval_provider = consume_context::<Rc<dyn EvalProvider>>();
|
||||
|
||||
let create_eval = Rc::new(move |script: &str| {
|
||||
eval_provider
|
||||
.new_evaluator(script.to_string())
|
||||
.map(UseEval::new)
|
||||
}) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>;
|
||||
UseEval::new(eval_provider.new_evaluator(script.to_string()))
|
||||
}) as Rc<dyn Fn(&str) -> UseEval>;
|
||||
|
||||
// Listen to server actions
|
||||
spawn({
|
||||
let timeline = timeline.clone();
|
||||
let action_rx = action_rx.clone();
|
||||
let create_eval = create_eval.clone();
|
||||
async move {
|
||||
let mut action_rx = action_rx.lock().expect("unpoisoned mutex");
|
||||
loop {
|
||||
let eval = action_rx.recv().await.expect("sender to exist");
|
||||
|
||||
let _ = match eval {
|
||||
Action::GoBack => create_eval(
|
||||
r#"
|
||||
|
@ -256,7 +253,7 @@ where
|
|||
history.length,
|
||||
];
|
||||
"#,
|
||||
).expect("failed to load state").await.expect("serializable state");
|
||||
).await.expect("serializable state");
|
||||
let (route, state, session, depth) = serde_json::from_value::<(
|
||||
String,
|
||||
Option<State>,
|
||||
|
@ -276,7 +273,8 @@ where
|
|||
// Call the updater callback
|
||||
(updater.read().unwrap())();
|
||||
|
||||
create_eval(&format!(r#"
|
||||
create_eval(&format!(
|
||||
r#"
|
||||
// this does not trigger a PopState event
|
||||
history.replaceState({state}, "", "{route}");
|
||||
sessionStorage.setItem("liveview", '{session}');
|
||||
|
@ -287,7 +285,8 @@ where
|
|||
event.state,
|
||||
]);
|
||||
}});
|
||||
"#)).expect("failed to initialize popstate")
|
||||
"#
|
||||
))
|
||||
};
|
||||
|
||||
loop {
|
||||
|
|
|
@ -289,17 +289,9 @@ pub trait HistoryProvider<R: Routable> {
|
|||
pub(crate) trait AnyHistoryProvider {
|
||||
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String>;
|
||||
|
||||
#[must_use]
|
||||
fn accepts_type_id(&self, type_id: &std::any::TypeId) -> bool;
|
||||
|
||||
#[must_use]
|
||||
fn current_route(&self) -> Rc<dyn Any>;
|
||||
|
||||
#[must_use]
|
||||
fn current_prefix(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn can_go_back(&self) -> bool {
|
||||
true
|
||||
|
@ -359,19 +351,11 @@ where
|
|||
.map(|route| Rc::new(route) as Rc<dyn Any>)
|
||||
}
|
||||
|
||||
fn accepts_type_id(&self, type_id: &std::any::TypeId) -> bool {
|
||||
type_id == &std::any::TypeId::of::<R>()
|
||||
}
|
||||
|
||||
fn current_route(&self) -> Rc<dyn Any> {
|
||||
let route = self.inner.current_route();
|
||||
Rc::new(route)
|
||||
}
|
||||
|
||||
fn current_prefix(&self) -> Option<String> {
|
||||
self.inner.current_prefix()
|
||||
}
|
||||
|
||||
fn can_go_back(&self) -> bool {
|
||||
self.inner.can_go_back()
|
||||
}
|
||||
|
|
|
@ -311,32 +311,6 @@ pub trait Routable: FromStr + Display + Clone + 'static {
|
|||
}
|
||||
}
|
||||
|
||||
trait RoutableFactory {
|
||||
type Err: Display;
|
||||
type Routable: Routable + FromStr<Err = Self::Err>;
|
||||
}
|
||||
|
||||
impl<R: Routable + FromStr> RoutableFactory for R
|
||||
where
|
||||
<R as FromStr>::Err: Display,
|
||||
{
|
||||
type Err = <R as FromStr>::Err;
|
||||
type Routable = R;
|
||||
}
|
||||
|
||||
trait RouteRenderable: Display + 'static {
|
||||
fn render(&self, level: usize) -> Element;
|
||||
}
|
||||
|
||||
impl<R: Routable> RouteRenderable for R
|
||||
where
|
||||
<R as FromStr>::Err: Display,
|
||||
{
|
||||
fn render(&self, level: usize) -> Element {
|
||||
self.render(level)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type erased map of the site structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SiteMapSegment {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::contexts::router::RoutingCallback;
|
||||
use crate::history::HistoryProvider;
|
||||
use crate::prelude::*;
|
||||
use crate::routable::Routable;
|
||||
use dioxus_lib::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Global configuration options for the router.
|
||||
///
|
||||
|
@ -31,43 +29,6 @@ pub struct RouterConfig<R: Routable> {
|
|||
pub(crate) initial_route: Option<R>,
|
||||
}
|
||||
|
||||
macro_rules! default_history {
|
||||
($initial_route:ident) => {
|
||||
{
|
||||
// If we are on wasm32 and the web feature is enabled, use the web history.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(WebHistory::<R>::default()));
|
||||
// If we are using dioxus fullstack and the ssr feature is enabled, use the memory history with the initial path set to the current path in fullstack
|
||||
#[cfg(all(feature = "fullstack", feature = "ssr"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(MemoryHistory::<R>::with_initial_path(
|
||||
dioxus_fullstack::prelude::server_context()
|
||||
.request_parts()
|
||||
.unwrap()
|
||||
.uri
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::error!("Failed to parse uri: {}", err);
|
||||
"/"
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("Failed to parse uri: {}", err);
|
||||
})
|
||||
}),
|
||||
)));
|
||||
// If we are not on wasm32 and the liveview feature is enabled, use the liveview history.
|
||||
#[cfg(all(feature = "liveview"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(LiveviewHistory::new_with_initial_path($initial_route)));
|
||||
// Otherwise use the memory history.
|
||||
#[cfg(all(
|
||||
not(all(target_arch = "wasm32", feature = "web")),
|
||||
not(all(feature = "liveview", not(target_arch = "wasm32"))),
|
||||
))]
|
||||
Box::new(AnyHistoryProviderImplWrapper::new(MemoryHistory::with_initial_path($initial_route)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<R: Routable + Clone> Default for RouterConfig<R>
|
||||
where
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
|
@ -94,7 +55,7 @@ where
|
|||
));
|
||||
self.history
|
||||
.take()
|
||||
.unwrap_or_else(|| default_history!(initial_route))
|
||||
.unwrap_or_else(|| default_history(initial_route))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,3 +115,47 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default history provider for the current platform.
|
||||
#[allow(unreachable_code, unused)]
|
||||
fn default_history<R: Routable + Clone>(initial_route: R) -> Box<dyn AnyHistoryProvider>
|
||||
where
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
// If we're on the web and have wasm, use the web history provider
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
WebHistory::<R>::default(),
|
||||
));
|
||||
|
||||
// If we're using fullstack and server side rendering, use the memory history provider
|
||||
#[cfg(all(feature = "fullstack", feature = "ssr"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
MemoryHistory::<R>::with_initial_path(
|
||||
dioxus_fullstack::prelude::server_context()
|
||||
.request_parts()
|
||||
.uri
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::error!("Failed to parse uri: {}", err);
|
||||
"/".parse().unwrap_or_else(|err| {
|
||||
panic!("Failed to parse uri: {}", err);
|
||||
})
|
||||
}),
|
||||
),
|
||||
));
|
||||
|
||||
// If liveview is enabled, use the liveview history provider
|
||||
#[cfg(feature = "liveview")]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
LiveviewHistory::new_with_initial_path(initial_route),
|
||||
));
|
||||
|
||||
// If none of the above, use the memory history provider, which is a decent enough fallback
|
||||
// Eventually we want to integrate with the mobile history provider, and other platform providers
|
||||
Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
MemoryHistory::with_initial_path(initial_route),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ proc-macro2 = "^1.0.63"
|
|||
quote = "^1.0.26"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
convert_case = "^0.6.0"
|
||||
server_fn_macro = "^0.5.2"
|
||||
server_fn_macro = "^0.6.5"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
axum = ["server_fn_macro/axum"]
|
||||
server = ["server_fn_macro/ssr"]
|
||||
|
|
|
@ -2,209 +2,83 @@
|
|||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
use convert_case::{Case, Converter};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Literal;
|
||||
use quote::{ToTokens, __private::TokenStream as TokenStream2};
|
||||
use server_fn_macro::*;
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
Ident, ItemFn, Token,
|
||||
};
|
||||
//! This crate contains the dioxus implementation of the #[macro@crate::server] macro without additional context from the server.
|
||||
//! See the [server_fn_macro] crate for more information.
|
||||
|
||||
/// Declares that a function is a [server function](https://dioxuslabs.com/learn/0.4/reference/fullstack/server_functions). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
use proc_macro::TokenStream;
|
||||
use server_fn_macro::server_macro_impl;
|
||||
use syn::__private::ToTokens;
|
||||
|
||||
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
|
||||
/// This means that its body will only run on the server, i.e., when the `ssr`
|
||||
/// feature is enabled on this crate.
|
||||
///
|
||||
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
|
||||
/// are enabled), it will instead make a network request to the server.
|
||||
///
|
||||
/// You can specify one, two, or three arguments to the server function:
|
||||
/// 1. *Optional*: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function
|
||||
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html),
|
||||
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
|
||||
/// server-side context into the server function.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use dioxus_fullstack::prelude::*; use serde::{Serialize, Deserialize};
|
||||
/// # #[derive(Serialize, Deserialize)]
|
||||
/// # pub struct Post { }
|
||||
/// #[server(ReadPosts, "/api")]
|
||||
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
|
||||
/// // do some work on the server to access the database
|
||||
/// # unimplemented!()
|
||||
/// ## Usage
|
||||
/// ```rust,ignore
|
||||
/// #[server]
|
||||
/// pub async fn blog_posts(
|
||||
/// category: String,
|
||||
/// ) -> Result<Vec<BlogPost>, ServerFnError> {
|
||||
/// let posts = load_posts(&category).await?;
|
||||
/// // maybe do some other work
|
||||
/// Ok(posts)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// - **The [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html). This scope can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
/// ## Named Arguments
|
||||
///
|
||||
/// You can any combination of the following named arguments:
|
||||
/// - `name`: sets the identifier for the server function’s type, which is a struct created
|
||||
/// to hold the arguments (defaults to the function identifier in PascalCase)
|
||||
/// - `prefix`: a prefix at which the server function handler will be mounted (defaults to `/api`)
|
||||
/// - `endpoint`: specifies the exact path at which the server function handler will be mounted,
|
||||
/// relative to the prefix (defaults to the function name followed by unique hash)
|
||||
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
|
||||
/// - `output`: the encoding for the response (defaults to `Json`)
|
||||
/// - `client`: a custom `Client` implementation that will be used for this server fn
|
||||
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one
|
||||
/// of the following (not case sensitive)
|
||||
/// - `"Url"`: `POST` request with URL-encoded arguments and JSON response
|
||||
/// - `"GetUrl"`: `GET` request with URL-encoded arguments and JSON response
|
||||
/// - `"Cbor"`: `POST` request with CBOR-encoded arguments and response
|
||||
/// - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
|
||||
/// - `req` and `res` specify the HTTP request and response types to be used on the server (these
|
||||
/// should usually only be necessary if you are integrating with a server other than Actix/Axum)
|
||||
/// ```rust,ignore
|
||||
/// #[server(
|
||||
/// name = SomeStructName,
|
||||
/// prefix = "/my_api",
|
||||
/// endpoint = "my_fn",
|
||||
/// input = Cbor,
|
||||
/// output = Json
|
||||
/// )]
|
||||
/// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> {
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// // expands to
|
||||
/// #[derive(Deserialize, Serialize)]
|
||||
/// struct SomeStructName {
|
||||
/// input: Vec<String>
|
||||
/// }
|
||||
///
|
||||
/// impl ServerFn for SomeStructName {
|
||||
/// const PATH: &'static str = "/my_api/my_fn";
|
||||
///
|
||||
/// // etc.
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
// before we pass this off to the server function macro, we apply extractors and middleware
|
||||
let mut function: syn::ItemFn = match syn::parse(s).map_err(|e| e.to_compile_error()) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return e.into(),
|
||||
};
|
||||
|
||||
// extract all #[middleware] attributes
|
||||
let mut middlewares: Vec<Middleware> = vec![];
|
||||
function.attrs.retain(|attr| {
|
||||
if attr.meta.path().is_ident("middleware") {
|
||||
if let Ok(middleware) = attr.parse_args() {
|
||||
middlewares.push(middleware);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
let ItemFn {
|
||||
attrs,
|
||||
vis,
|
||||
sig,
|
||||
block,
|
||||
} = function;
|
||||
let mapped_body = quote::quote! {
|
||||
#(#attrs)*
|
||||
#vis #sig {
|
||||
#block
|
||||
}
|
||||
};
|
||||
|
||||
let server_fn_path: syn::Path = syn::parse_quote!(::dioxus::fullstack::prelude::server_fn);
|
||||
let trait_obj_wrapper: syn::Type =
|
||||
syn::parse_quote!(::dioxus::fullstack::prelude::ServerFnTraitObj);
|
||||
let mut args: ServerFnArgs = match syn::parse(args) {
|
||||
Ok(args) => args,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
if args.struct_name.is_none() {
|
||||
let upper_cammel_case_name = Converter::new()
|
||||
.from_case(Case::Snake)
|
||||
.to_case(Case::UpperCamel)
|
||||
.convert(sig.ident.to_string());
|
||||
args.struct_name = Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
|
||||
}
|
||||
let struct_name = args.struct_name.as_ref().unwrap();
|
||||
match server_macro_impl(
|
||||
quote::quote!(#args),
|
||||
mapped_body,
|
||||
trait_obj_wrapper,
|
||||
args.into(),
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(server_fn)),
|
||||
"/api",
|
||||
None,
|
||||
None,
|
||||
Some(server_fn_path.clone()),
|
||||
) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(tokens) => quote::quote! {
|
||||
#tokens
|
||||
#[cfg(feature = "server")]
|
||||
#server_fn_path::inventory::submit! {
|
||||
::dioxus::fullstack::prelude::ServerFnMiddleware {
|
||||
prefix: #struct_name::PREFIX,
|
||||
url: #struct_name::URL,
|
||||
middleware: || vec![
|
||||
#(
|
||||
std::sync::Arc::new(#middlewares),
|
||||
),*
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
.to_token_stream()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Middleware {
|
||||
expr: syn::Expr,
|
||||
}
|
||||
|
||||
impl ToTokens for Middleware {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let expr = &self.expr;
|
||||
tokens.extend(quote::quote! {
|
||||
#expr
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Middleware {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let arg: syn::Expr = input.parse()?;
|
||||
Ok(Middleware { expr: arg })
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerFnArgs {
|
||||
struct_name: Option<Ident>,
|
||||
_comma: Option<Token![,]>,
|
||||
prefix: Option<Literal>,
|
||||
_comma2: Option<Token![,]>,
|
||||
encoding: Option<Literal>,
|
||||
_comma3: Option<Token![,]>,
|
||||
fn_path: Option<Literal>,
|
||||
}
|
||||
|
||||
impl ToTokens for ServerFnArgs {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let struct_name = self.struct_name.as_ref().map(|s| quote::quote! { #s, });
|
||||
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
|
||||
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
|
||||
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
|
||||
tokens.extend(quote::quote! {
|
||||
#struct_name
|
||||
#prefix
|
||||
#encoding
|
||||
#fn_path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ServerFnArgs {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let struct_name = input.parse()?;
|
||||
let _comma = input.parse()?;
|
||||
let prefix = input.parse()?;
|
||||
let _comma2 = input.parse()?;
|
||||
let encoding = input.parse()?;
|
||||
let _comma3 = input.parse()?;
|
||||
let fn_path = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
struct_name,
|
||||
_comma,
|
||||
prefix,
|
||||
_comma2,
|
||||
encoding,
|
||||
_comma3,
|
||||
fn_path,
|
||||
})
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ generational-box = { workspace = true }
|
|||
askama_escape = "0.10.3"
|
||||
thiserror = "1.0.23"
|
||||
rustc-hash = "1.1.0"
|
||||
lru = "0.10.0"
|
||||
lru = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
http = "0.2.9"
|
||||
async-trait = "0.1.58"
|
||||
serde_json = { version = "1.0" }
|
||||
http = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
chrono = { verison = "0.4.34", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
|
@ -30,6 +30,8 @@ tokio = { version = "1.28", features = ["fs", "io-util"], optional = true }
|
|||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
dioxus-signals = { workspace = true }
|
||||
|
||||
tracing = { workspace = true }
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
anyhow = "1.0"
|
||||
|
@ -37,7 +39,6 @@ argh = "0.1.4"
|
|||
serde = "1.0.120"
|
||||
serde_json = "1.0.61"
|
||||
fs_extra = "1.2.0"
|
||||
dioxus-signals = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -103,6 +103,7 @@ impl ValidCachedPath {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn freshness(&self, max_age: Option<std::time::Duration>) -> Option<RenderFreshness> {
|
||||
let age = self.timestamp.elapsed().ok()?.as_secs();
|
||||
let max_age = max_age.map(|max_age| max_age.as_secs());
|
||||
|
|
|
@ -10,13 +10,12 @@ use rustc_hash::FxHasher;
|
|||
use std::{
|
||||
future::Future,
|
||||
hash::BuildHasherDefault,
|
||||
io::Write,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt, BufReader};
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||
|
||||
pub use crate::fs_cache::*;
|
||||
pub use crate::incremental_cfg::*;
|
||||
|
@ -67,6 +66,7 @@ impl IncrementalRenderer {
|
|||
let _ = std::fs::remove_dir_all(&self.static_dir);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn track_timestamps(&self) -> bool {
|
||||
self.invalidate_after.is_some()
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ impl IncrementalRenderer {
|
|||
) -> Result<RenderFreshness, IncrementalRendererError> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use std::io::Write;
|
||||
let file_path = self.route_as_path(&route);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
if !parent.exists() {
|
||||
|
@ -122,6 +123,7 @@ impl IncrementalRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn promote_memory_cache<K: AsRef<str>>(&mut self, route: K) {
|
||||
if let Some(cache) = self.memory_cache.as_mut() {
|
||||
cache.promote(route.as_ref())
|
||||
|
@ -160,7 +162,7 @@ impl IncrementalRenderer {
|
|||
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 {
|
||||
let mut file = BufReader::new(file);
|
||||
let mut file = tokio::io::BufReader::new(file);
|
||||
tokio::io::copy_buf(&mut file, output).await?;
|
||||
tracing::trace!("file cache hit {:?}", route);
|
||||
self.promote_memory_cache(&route);
|
||||
|
@ -229,6 +231,7 @@ impl IncrementalRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn route_as_path(&self, route: &str) -> PathBuf {
|
||||
let mut file_path = (self.map_path)(route);
|
||||
if self.track_timestamps() {
|
||||
|
|
Loading…
Reference in a new issue