codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)
overhaul dioxus fullstack using a new implementation of server_fn This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -207,7 +207,7 @@ fn get_mime_by_ext(trimmed: &Path) -> &'static str {
|
||||
}
|
||||
|
||||
/// Get the mime type from a URI using its extension
|
||||
fn get_mime_from_ext(ext: Option<&str>) -> &'static str {
|
||||
pub fn get_mime_from_ext(ext: Option<&str>) -> &'static str {
|
||||
match ext {
|
||||
// The common assets are all utf-8 encoded
|
||||
Some("js") => "text/javascript; charset=utf-8",
|
||||
|
||||
@@ -222,6 +222,7 @@ pub fn is_cli_enabled() -> bool {
|
||||
/// be served at `http://localhost:8080/dogapp`.
|
||||
///
|
||||
/// All assets will be served from this base path as well, ie `http://localhost:8080/dogapp/assets/logo.png`.
|
||||
#[allow(unreachable_code)]
|
||||
pub fn base_path() -> Option<String> {
|
||||
// This may trigger when compiling to the server if you depend on another crate that pulls in
|
||||
// the web feature. It might be better for the renderers to provide the current platform
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
!README.md
|
||||
!.gitignore
|
||||
@@ -0,0 +1,65 @@
|
||||
# Test harnesses
|
||||
|
||||
This folder contains a series of CLI harnesses that represent common structures of dioxus apps.
|
||||
|
||||
We test these projects as a form of smoke testing to ensure our argument resolution works properly.
|
||||
|
||||
These projects might not be functionally useful, but they do have interesting properties that the CLI tests.
|
||||
|
||||
The tests for the CLI are contained within the CLI itself.
|
||||
|
||||
## This Folder
|
||||
|
||||
The items in this folder are procedurally generated by tests in the CLI crate. We will delete EVERYTHING that ends up here.
|
||||
|
||||
## How platform resolution works
|
||||
|
||||
Determine which features, triple, profile, etc to pass to the build.
|
||||
|
||||
Most of the time, users should use `dx serve --<platform>` where the platform name directly
|
||||
corresponds to the feature in their cargo.toml. So,
|
||||
- `dx serve --web` will enable the `web` feature
|
||||
- `dx serve --mobile` will enable the `mobile` feature
|
||||
- `dx serve --desktop` will enable the `desktop` feature
|
||||
|
||||
In this case, we set default-features to false and then add back the default features that
|
||||
aren't renderers, and then add the feature for the given renderer (ie web/desktop/mobile).
|
||||
We call this "no-default-features-stripped."
|
||||
|
||||
There are a few cases where the user doesn't need to pass a platform.
|
||||
- they selected one via `dioxus = { features = ["web"] }`
|
||||
- they have a single platform in their default features `default = ["web"]`
|
||||
- there is only a single non-server renderer as a feature `web = ["dioxus/web"], server = ["dioxus/server"]`
|
||||
- they compose the super triple via triple + bundleformat + features
|
||||
|
||||
Note that we only use the names of the features to correspond with the platform.
|
||||
Platforms are "super triples", meaning they contain information about
|
||||
- bundle format
|
||||
- target triple
|
||||
- how to serve
|
||||
- enabled features
|
||||
|
||||
By default, the --platform presets correspond to:
|
||||
- web: bundle(web), triple(wasm32), serve(http-serve), features("web")
|
||||
- desktop: alias to mac/win/linux
|
||||
- mac: bundle(mac), triple(host), serve(appbundle-open), features("desktop")
|
||||
- windows: bundle(exefolder), triple(host), serve(run-exe), features("desktop")
|
||||
- linux: bundle(appimage), triple(host), serve(run-exe), features("desktop")
|
||||
- ios: bundle(ios), triple(arm64-apple-ios), serve(ios-simulator/xcrun), features("mobile")
|
||||
- android: bundle(android), triple(arm64-apple-ios), serve(android-emulator/adb), features("mobile")
|
||||
- server: bundle(server), triple(host), serve(run-exe), features("server")
|
||||
- liveview: bundle(liveview), triple(host), serve(run-exe), features("liveview")
|
||||
- unknown: <auto or default to desktop>
|
||||
|
||||
Fullstack usage is inferred from the presence of the fullstack feature or --fullstack.
|
||||
|
||||
## List of harnesses (brainstorming)
|
||||
|
||||
- app that doesn't use dioxus
|
||||
- simple app where the renderer is used as an explicit feature
|
||||
- simple app, same as above, but with fullstack enabled
|
||||
- since no `server` feature is present, we don't run the server
|
||||
- simple app, same as above, but with a `server` feature
|
||||
- server should launch and take precedence over the client
|
||||
- simple server-only app
|
||||
- IE you're doing SSR with dioxus
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "harness-default-to-non-default"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = [] }
|
||||
|
||||
|
||||
[features]
|
||||
default=["web"]
|
||||
web=["dioxus/web"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "harness-fullstack-desktop-with-default"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
anyhow = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
default=["desktop"]
|
||||
desktop=["dioxus/desktop", "has_anyhow"]
|
||||
has_anyhow=["dep:anyhow"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "harness-fullstack-desktop-with-features"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
anyhow = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
desktop=["dioxus/desktop", "has_anyhow"]
|
||||
has_anyhow=["dep:anyhow"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "harness-fullstack-desktop"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
desktop=["dioxus/desktop"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "harness-fullstack-multi-target-no-default"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
web=["dioxus/web"]
|
||||
desktop=["dioxus/desktop"]
|
||||
mobile=["dioxus/mobile"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "harness-fullstack-multi-target"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
default=["web", "desktop", "mobile", "server"]
|
||||
web=["dioxus/web"]
|
||||
desktop=["dioxus/desktop"]
|
||||
mobile=["dioxus/mobile"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "harness-fullstack-with-optional-tokio"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
serde = "1.0.219"
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
server = ["dioxus/server", "dep:tokio"]
|
||||
web = ["dioxus/web"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "harness-no-dioxus"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
web=["dep:anyhow"]
|
||||
server=[]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "harness-renderer-swap"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
default=["desktop", "server"]
|
||||
desktop=["dioxus/desktop"]
|
||||
native=["dioxus/native"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "harness-simple-dedicated-client"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["web"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "harness-simple-dedicated-server"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "harness-simple-desktop"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["desktop"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "harness-simple-fullstack-native-with-default"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
default=["native", "server"]
|
||||
native=["dioxus/native"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "harness-simple-fullstack-with-default"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
default=["web", "server"]
|
||||
web=["dioxus/web"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "harness-simple-fullstack"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
|
||||
[features]
|
||||
web=["dioxus/web"]
|
||||
server=["dioxus/server"]
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "harness-simple-mobile"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["mobile"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "harness-simple-web"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["web"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
@@ -47,6 +47,7 @@ fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash>
|
||||
// Create a hasher
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
options.hash(&mut hash);
|
||||
|
||||
// Hash the version of CLI opt
|
||||
hash.write(crate::build_info::version().as_bytes());
|
||||
hash_file_with_options(options, source, &mut hash, false)?;
|
||||
@@ -70,6 +71,7 @@ pub(crate) fn hash_file_with_options(
|
||||
ResolvedAssetType::Scss(options) => {
|
||||
hash_scss(options, source, hasher)?;
|
||||
}
|
||||
|
||||
ResolvedAssetType::Js(options) => {
|
||||
hash_js(options, source, hasher, !in_folder)?;
|
||||
}
|
||||
@@ -82,12 +84,24 @@ pub(crate) fn hash_file_with_options(
|
||||
| ResolvedAssetType::File => {
|
||||
hash_file_contents(source, hasher)?;
|
||||
}
|
||||
|
||||
// Or the folder contents recursively
|
||||
ResolvedAssetType::Folder(_) => {
|
||||
let files = std::fs::read_dir(source)?;
|
||||
for file in files.flatten() {
|
||||
for file in std::fs::read_dir(source)?.flatten() {
|
||||
let path = file.path();
|
||||
hash_file_with_options(options, &path, hasher, true)?;
|
||||
hash_file_with_options(
|
||||
// We can't reuse the options here since they contain the source variant which no
|
||||
// longer applies to the nested files
|
||||
//
|
||||
// We don't hash nested files either since we assume the parent here is already being hashed
|
||||
// (or being opted out of hashing)
|
||||
&AssetOptions::builder()
|
||||
.with_hash_suffix(false)
|
||||
.into_asset_options(),
|
||||
&path,
|
||||
hasher,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +140,7 @@ pub fn add_hash_to_asset(asset: &mut BundledAsset) {
|
||||
tracing::error!("Failed to get file name from path: {source}");
|
||||
return;
|
||||
};
|
||||
|
||||
// The output extension path is the extension set by the options
|
||||
// or the extension of the source file if we don't recognize the file
|
||||
let mut ext = asset.options().extension().map(Into::into).or_else(|| {
|
||||
@@ -163,7 +178,7 @@ pub fn add_hash_to_asset(asset: &mut BundledAsset) {
|
||||
*asset = BundledAsset::new(source, &bundled_path, options);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to hash asset: {err}");
|
||||
tracing::error!("Failed to hash asset {source}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,7 +320,10 @@ pub(crate) async fn extract_assets_from_file(path: impl AsRef<Path>) -> Result<A
|
||||
|
||||
if let Some((_, bundled_asset)) = const_serialize::deserialize_const!(BundledAsset, buffer)
|
||||
{
|
||||
tracing::debug!("Found asset at offset {offset}: {:?}", bundled_asset);
|
||||
tracing::debug!(
|
||||
"Found asset at offset {offset}: {:?}",
|
||||
bundled_asset.absolute_source_path()
|
||||
);
|
||||
assets.push(bundled_asset);
|
||||
} else {
|
||||
tracing::warn!("Found an asset at offset {offset} that could not be deserialized. This may be caused by a mismatch between your dioxus and dioxus-cli versions.");
|
||||
|
||||
@@ -715,7 +715,7 @@ impl AppBuilder {
|
||||
|
||||
let changed_file = changed_files.first().unwrap();
|
||||
tracing::info!(
|
||||
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#}",
|
||||
"Hot-patching: {NOTE_STYLE}{}{NOTE_STYLE:#} took {GLOW_STYLE}{:?}ms{GLOW_STYLE:#} ({})",
|
||||
changed_file
|
||||
.display()
|
||||
.to_string()
|
||||
@@ -723,7 +723,8 @@ impl AppBuilder {
|
||||
SystemTime::now()
|
||||
.duration_since(res.time_start)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
.as_millis(),
|
||||
triple
|
||||
);
|
||||
|
||||
self.patches.push(jump_table.clone());
|
||||
|
||||
@@ -1191,6 +1191,8 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut exported = HashSet::new();
|
||||
|
||||
// Wasm-bindgen will synthesize imports to satisfy its external calls. This facilitates things
|
||||
// like inline-js, snippets, and literally the `#[wasm_bindgen]` macro. All calls to JS are
|
||||
// just `extern "wbg"` blocks!
|
||||
@@ -1227,9 +1229,10 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
let new_func_id = module.funcs.add_local(builder.local_func(locals));
|
||||
|
||||
module
|
||||
.exports
|
||||
.add(&format!("__saved_wbg_{}", import.name), new_func_id);
|
||||
let saved_name = format!("__saved_wbg_{}", import.name);
|
||||
if exported.insert(saved_name.clone()) {
|
||||
module.exports.add(&saved_name, new_func_id);
|
||||
}
|
||||
|
||||
make_indirect.push(new_func_id);
|
||||
}
|
||||
@@ -1254,9 +1257,10 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result<Vec<u8>> {
|
||||
//
|
||||
// https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1505
|
||||
if name.starts_with("__wbindgen") {
|
||||
module
|
||||
.exports
|
||||
.add(&format!("__saved_wbg_{name}"), func.id());
|
||||
let saved_name = format!("__saved_wbg_{}", name);
|
||||
if exported.insert(saved_name.clone()) {
|
||||
module.exports.add(&saved_name, func.id());
|
||||
}
|
||||
}
|
||||
|
||||
// This is basically `--export-all` but designed to work around wasm-bindgen not properly gc-ing
|
||||
|
||||
+297
-107
@@ -321,7 +321,7 @@
|
||||
|
||||
use crate::{
|
||||
AndroidTools, BuildContext, BundleFormat, DioxusConfig, Error, LinkAction, LinkerFlavor,
|
||||
Renderer, Result, RustcArgs, TargetAlias, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
|
||||
Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig,
|
||||
Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
@@ -348,7 +348,7 @@ use std::{
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
|
||||
use target_lexicon::{Architecture, OperatingSystem, Triple};
|
||||
use tempfile::TempDir;
|
||||
use tokio::{io::AsyncBufReadExt, process::Command};
|
||||
use uuid::Uuid;
|
||||
@@ -549,105 +549,257 @@ impl BuildRequest {
|
||||
})?
|
||||
.clone();
|
||||
|
||||
// The crate might enable multiple platforms or no platforms at
|
||||
// We collect all the platforms it enables first and then select based on the --platform arg
|
||||
let enabled_renderers =
|
||||
Self::enabled_cargo_toml_renderers(main_package, args.no_default_features);
|
||||
let using_dioxus_explicitly = main_package
|
||||
.dependencies
|
||||
.iter()
|
||||
.any(|dep| dep.name == "dioxus");
|
||||
|
||||
let mut features = args.features.clone();
|
||||
let mut no_default_features = args.no_default_features;
|
||||
|
||||
// Apply the platform alias if present
|
||||
let mut target_alias = args.target_alias;
|
||||
let mut renderer = args.renderer;
|
||||
let mut bundle = args.bundle;
|
||||
if let Some(platform) = args.platform {
|
||||
let (default_target, default_renderer, default_bundle) = platform.into_triple();
|
||||
target_alias = target_alias.or(default_target);
|
||||
renderer.renderer = Some(renderer.renderer.unwrap_or(default_renderer));
|
||||
bundle = Some(bundle.unwrap_or(default_bundle));
|
||||
}
|
||||
|
||||
let mut renderer: Option<Renderer> = match renderer.into() {
|
||||
Some(renderer) => match enabled_renderers.len() {
|
||||
0 => Some(renderer),
|
||||
|
||||
// The user passed --platform XYZ but already has `default = ["ABC"]` in their Cargo.toml or dioxus = { features = ["abc"] }
|
||||
// We want to strip out the default platform and use the one they passed, setting no-default-features
|
||||
_ => {
|
||||
features.extend(Self::rendererless_features(main_package));
|
||||
no_default_features = true;
|
||||
Some(renderer)
|
||||
}
|
||||
},
|
||||
None if !using_dioxus_explicitly => None,
|
||||
None => match enabled_renderers.as_slice() {
|
||||
[] => None, // Wait until we resolve everything else first then we will resolve it from the triple
|
||||
[(renderer, feature)] => {
|
||||
let targeting_mobile = match (&args.target, target_alias) {
|
||||
(_, TargetAlias::Android | TargetAlias::Ios) => true,
|
||||
(Some(target), _) => {
|
||||
matches!(
|
||||
(target.environment, target.operating_system),
|
||||
(Environment::Android, OperatingSystem::IOS(_))
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
// The renderer usually maps directly to a feature flag, but the mobile crate is an exception.
|
||||
// If we see the `desktop` renderer in the default features, but the user is targeting mobile,
|
||||
// we should remove the default `desktop` feature, but still target webview. The feature selection
|
||||
// logic below will handle adding back in the `mobile` feature flag
|
||||
if targeting_mobile && feature == "desktop" {
|
||||
features.extend(Self::rendererless_features(main_package));
|
||||
no_default_features = true;
|
||||
}
|
||||
Some(*renderer)
|
||||
}
|
||||
_ => {
|
||||
bail!(
|
||||
"Multiple platforms enabled in Cargo.toml. Please specify a platform with `--web`, `--webview`, or `--native` or set a default platform in Cargo.toml"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// We usually use the simulator unless --device is passed *or* a device is detected by probing.
|
||||
// For now, though, since we don't have probing, it just defaults to false
|
||||
// Tools like xcrun/adb can detect devices
|
||||
let device = args.device.clone();
|
||||
|
||||
// Resolve the target alias and renderer into a concrete target alias.
|
||||
// If the user didn't pass a platform, but we have a renderer, get the default platform for that renderer
|
||||
if let (TargetAlias::Unknown, Some(renderer)) = (target_alias, renderer) {
|
||||
target_alias = renderer.default_platform();
|
||||
let using_dioxus_explicitly = main_package
|
||||
.dependencies
|
||||
.iter()
|
||||
.any(|dep| dep.name == "dioxus");
|
||||
|
||||
/*
|
||||
Determine which features, triple, profile, etc to pass to the build.
|
||||
|
||||
Most of the time, users should use `dx serve --<platform>` where the platform name directly
|
||||
corresponds to the feature in their cargo.toml. So,
|
||||
- `dx serve --web` will enable the `web` feature
|
||||
- `dx serve --mobile` will enable the `mobile` feature
|
||||
- `dx serve --desktop` will enable the `desktop` feature
|
||||
|
||||
In this case, we set default-features to false and then add back the default features that
|
||||
aren't renderers, and then add the feature for the given renderer (ie web/desktop/mobile).
|
||||
We call this "no-default-features-stripped."
|
||||
|
||||
There are a few cases where the user doesn't need to pass a platform.
|
||||
- they selected one via `dioxus = { features = ["web"] }`
|
||||
- they have a single platform in their default features `default = ["web"]`
|
||||
- there is only a single non-server renderer as a feature `web = ["dioxus/web"], server = ["dioxus/server"]`
|
||||
- they compose the super triple via triple + bundleformat + features
|
||||
|
||||
Note that we only use the names of the features to correspond with the platform.
|
||||
Platforms are "super triples", meaning they contain information about
|
||||
- bundle format
|
||||
- target triple
|
||||
- how to serve
|
||||
- enabled features
|
||||
|
||||
By default, the --platform presets correspond to:
|
||||
- web: bundle(web), triple(wasm32), serve(http-serve), features("web")
|
||||
- desktop: alias to mac/win/linux
|
||||
- mac: bundle(mac), triple(host), serve(appbundle-open), features("desktop")
|
||||
- windows: bundle(exefolder), triple(host), serve(run-exe), features("desktop")
|
||||
- linux: bundle(appimage), triple(host), serve(run-exe), features("desktop")
|
||||
- ios: bundle(ios), triple(arm64-apple-ios), serve(ios-simulator/xcrun), features("mobile")
|
||||
- android: bundle(android), triple(arm64-apple-ios), serve(android-emulator/adb), features("mobile")
|
||||
- server: bundle(server), triple(host), serve(run-exe), features("server") (and disables the client)
|
||||
- liveview: bundle(liveview), triple(host), serve(run-exe), features("liveview")
|
||||
- unknown: <auto or default to desktop>
|
||||
|
||||
Fullstack usage is inferred from the presence of the fullstack feature or --fullstack.
|
||||
*/
|
||||
let mut features = args.features.clone();
|
||||
let mut no_default_features = args.no_default_features;
|
||||
let mut triple = args.target.clone();
|
||||
let mut renderer = args.renderer;
|
||||
let mut bundle_format = args.bundle;
|
||||
let mut platform = args.platform;
|
||||
|
||||
// the crate might be selecting renderers but the user also passes a renderer. this is weird
|
||||
// ie dioxus = { features = ["web"] } but also --platform desktop
|
||||
// anyways, we collect it here in the event we need it if platform is not specified.
|
||||
let dioxus_direct_renderer = Self::renderer_enabled_by_dioxus_dependency(main_package);
|
||||
let known_features_as_renderers = Self::features_that_enable_renderers(main_package);
|
||||
|
||||
// The crate might enable multiple platforms or no platforms at
|
||||
// We collect all the platforms it enables first and then select based on the --platform arg
|
||||
let enabled_renderers = if no_default_features {
|
||||
vec![]
|
||||
} else {
|
||||
Self::enabled_cargo_toml_default_features_renderers(main_package)
|
||||
};
|
||||
|
||||
// We want a real triple to build with, so we'll autodetect it if it's not provided
|
||||
// The triple ends up being a source of truth for us later hence all this work to figure it out
|
||||
let triple = match (args.target.clone(), target_alias) {
|
||||
// If there is an explicit target, use it
|
||||
(Some(target), _) => target,
|
||||
// If there is a platform, use it to determine the target triple
|
||||
(None, platform) => platform.into_target(device.is_some(), &workspace).await?,
|
||||
};
|
||||
// Try the easy autodetects.
|
||||
// - if the user has `dioxus = { features = ["web"] }`
|
||||
// - if the `default =["web"]` or `default = ["dioxus/web"]`
|
||||
// - if there's only one non-server platform ie `web = ["dioxus/web"], server = ["dioxus/server"]`
|
||||
// Only do this if we're explicitly using dioxus
|
||||
if matches!(platform, Platform::Unknown) && using_dioxus_explicitly {
|
||||
let auto = dioxus_direct_renderer
|
||||
.or_else(|| {
|
||||
if enabled_renderers.len() == 1 {
|
||||
Some(enabled_renderers[0].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
let non_server_features = known_features_as_renderers
|
||||
.iter()
|
||||
.filter(|f| f.1.as_str() != "server")
|
||||
.collect::<Vec<_>>();
|
||||
if non_server_features.len() == 1 {
|
||||
Some(non_server_features[0].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// If the user didn't pass a renderer, and we didn't find a good default renderer, but they are using dioxus explicitly,
|
||||
// then we can try to guess the renderer based on the target triple.
|
||||
if renderer.is_none() && using_dioxus_explicitly {
|
||||
renderer = Some(Renderer::from_target(&triple));
|
||||
if let Some((direct, feature)) = auto {
|
||||
match direct {
|
||||
_ if feature == "mobile" || feature == "dioxus/mobile" => {
|
||||
bail!(
|
||||
"Could not autodetect mobile platform. Use --ios or --android instead."
|
||||
);
|
||||
}
|
||||
Renderer::Webview | Renderer::Native => {
|
||||
if cfg!(target_os = "macos") {
|
||||
platform = Platform::MacOS;
|
||||
} else if cfg!(target_os = "linux") {
|
||||
platform = Platform::Linux;
|
||||
} else if cfg!(target_os = "windows") {
|
||||
platform = Platform::Windows;
|
||||
}
|
||||
}
|
||||
Renderer::Server => platform = Platform::Server,
|
||||
Renderer::Liveview => platform = Platform::Liveview,
|
||||
Renderer::Web => platform = Platform::Web,
|
||||
}
|
||||
renderer = renderer.or(Some(direct));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the bundle format based on the combination of the target triple and renderer
|
||||
let bundle = match bundle {
|
||||
// If there is an explicit bundle format, use it
|
||||
Some(bundle) => bundle,
|
||||
// Otherwise guess a bundle format based on the target triple and renderer
|
||||
None => BundleFormat::from_target(&triple, renderer)?,
|
||||
// Set the super triple from the platform if it's provided.
|
||||
// Otherwise, we attempt to guess it from the rest of their inputs.
|
||||
match platform {
|
||||
Platform::Unknown => {}
|
||||
|
||||
Platform::Web => {
|
||||
if main_package.features.contains_key("web") && renderer.is_none() {
|
||||
features.push("web".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Web));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Web));
|
||||
triple = triple.or(Some("wasm32-unknown-unknown".parse()?));
|
||||
no_default_features = true;
|
||||
}
|
||||
Platform::MacOS => {
|
||||
if main_package.features.contains_key("desktop") && renderer.is_none() {
|
||||
features.push("desktop".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Webview));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::MacOS));
|
||||
triple = triple.or(Some(Triple::host()));
|
||||
no_default_features = true;
|
||||
}
|
||||
Platform::Windows => {
|
||||
if main_package.features.contains_key("desktop") && renderer.is_none() {
|
||||
features.push("desktop".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Webview));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Windows));
|
||||
triple = triple.or(Some(Triple::host()));
|
||||
no_default_features = true;
|
||||
}
|
||||
Platform::Linux => {
|
||||
if main_package.features.contains_key("desktop") && renderer.is_none() {
|
||||
features.push("desktop".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Webview));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Linux));
|
||||
triple = triple.or(Some(Triple::host()));
|
||||
no_default_features = true;
|
||||
}
|
||||
Platform::Ios => {
|
||||
if main_package.features.contains_key("mobile") && renderer.is_none() {
|
||||
features.push("mobile".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Webview));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Ios));
|
||||
no_default_features = true;
|
||||
match device.is_some() {
|
||||
// If targeting device, we want to build for the device which is always aarch64
|
||||
true => triple = triple.or(Some("aarch64-apple-ios".parse()?)),
|
||||
|
||||
// If the host is aarch64, we assume the user wants to build for iOS simulator
|
||||
false if matches!(Triple::host().architecture, Architecture::Aarch64(_)) => {
|
||||
triple = triple.or(Some("aarch64-apple-ios-sim".parse()?))
|
||||
}
|
||||
|
||||
// Otherwise, it's the x86_64 simulator, which is just x86_64-apple-ios
|
||||
_ => triple = triple.or(Some("x86_64-apple-ios".parse()?)),
|
||||
}
|
||||
}
|
||||
Platform::Android => {
|
||||
if main_package.features.contains_key("mobile") && renderer.is_none() {
|
||||
features.push("mobile".into());
|
||||
}
|
||||
|
||||
renderer = renderer.or(Some(Renderer::Webview));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Android));
|
||||
no_default_features = true;
|
||||
|
||||
// maybe probe adb?
|
||||
if let Some(_device_name) = device.as_ref() {
|
||||
if triple.is_none() {
|
||||
triple = Some(
|
||||
crate::get_android_tools()
|
||||
.context("Failed to get android tools")?
|
||||
.autodetect_android_device_triple()
|
||||
.await,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
triple = triple.or(Some({
|
||||
match Triple::host().architecture {
|
||||
Architecture::X86_32(_) => "i686-linux-android".parse()?,
|
||||
Architecture::X86_64 => "x86_64-linux-android".parse()?,
|
||||
Architecture::Aarch64(_) => "aarch64-linux-android".parse()?,
|
||||
_ => "aarch64-linux-android".parse()?,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
Platform::Server => {
|
||||
if main_package.features.contains_key("server") && renderer.is_none() {
|
||||
features.push("server".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Server));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Server));
|
||||
triple = triple.or(Some(Triple::host()));
|
||||
no_default_features = true;
|
||||
}
|
||||
Platform::Liveview => {
|
||||
if main_package.features.contains_key("liveview") && renderer.is_none() {
|
||||
features.push("liveview".into());
|
||||
}
|
||||
renderer = renderer.or(Some(Renderer::Liveview));
|
||||
bundle_format = bundle_format.or(Some(BundleFormat::Server));
|
||||
triple = triple.or(Some(Triple::host()));
|
||||
no_default_features = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no default features are enabled, we need to add the rendererless features
|
||||
if no_default_features {
|
||||
features.extend(Self::rendererless_features(main_package));
|
||||
features.dedup();
|
||||
features.sort();
|
||||
}
|
||||
|
||||
// The triple will be the triple passed or the host if using dioxus.
|
||||
let triple = if using_dioxus_explicitly {
|
||||
triple.context("Could not automatically detect target triple")?
|
||||
} else {
|
||||
triple.unwrap_or(Triple::host())
|
||||
};
|
||||
|
||||
// The bundle format will be the bundle format passed or the host.
|
||||
let bundle = if using_dioxus_explicitly {
|
||||
bundle_format.context("Could not automatically detect bundle format")?
|
||||
} else {
|
||||
bundle_format.unwrap_or(BundleFormat::host())
|
||||
};
|
||||
|
||||
// Add any features required to turn on the client
|
||||
@@ -657,6 +809,7 @@ impl BuildRequest {
|
||||
&triple,
|
||||
renderer,
|
||||
));
|
||||
features.dedup();
|
||||
}
|
||||
|
||||
// Set the profile of the build if it's not already set
|
||||
@@ -714,6 +867,14 @@ impl BuildRequest {
|
||||
]);
|
||||
}
|
||||
|
||||
// automatically set the getrandom backend for web builds if the user requested it
|
||||
if matches!(bundle, BundleFormat::Web) && args.wasm_js_cfg {
|
||||
rustflags.flags.extend(
|
||||
cargo_config2::Flags::from_space_separated(r#"--cfg getrandom_backend="wasm_js""#)
|
||||
.flags,
|
||||
);
|
||||
}
|
||||
|
||||
// Make sure to take into account the RUSTFLAGS env var and the CARGO_TARGET_<triple>_RUSTFLAGS
|
||||
for env in [
|
||||
"RUSTFLAGS".to_string(),
|
||||
@@ -1032,7 +1193,11 @@ impl BuildRequest {
|
||||
// since that is a really bad user experience.
|
||||
Message::BuildFinished(finished) => {
|
||||
if !finished.success {
|
||||
bail!("cargo build finished with errors.")
|
||||
bail!(
|
||||
"cargo build finished with errors for target: {} [{}]",
|
||||
self.main_target,
|
||||
self.triple
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -1190,7 +1355,6 @@ impl BuildRequest {
|
||||
|
||||
async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
|
||||
let framework_dir = self.frameworks_folder();
|
||||
_ = std::fs::create_dir_all(&framework_dir);
|
||||
|
||||
// We have some prebuilt stuff that needs to be copied into the framework dir
|
||||
let openssl_dir = AndroidTools::openssl_lib_dir(&self.triple);
|
||||
@@ -1208,6 +1372,8 @@ impl BuildRequest {
|
||||
|
||||
tracing::debug!("Copying framework from {from:?} to {to:?}");
|
||||
|
||||
_ = std::fs::create_dir_all(&framework_dir);
|
||||
|
||||
// in dev and on normal oses, we want to symlink the file
|
||||
// otherwise, just copy it (since in release you want to distribute the framework)
|
||||
if cfg!(any(windows, unix)) && !self.release {
|
||||
@@ -1225,6 +1391,11 @@ impl BuildRequest {
|
||||
}
|
||||
}
|
||||
|
||||
// Always create the framework dir for android
|
||||
if self.bundle == BundleFormat::Android {
|
||||
_ = std::fs::create_dir_all(&framework_dir);
|
||||
}
|
||||
|
||||
// On android, the c++_shared flag means we need to copy the libc++_shared.so precompiled
|
||||
// library to the jniLibs folder
|
||||
if self.bundle == BundleFormat::Android && arg.contains("-lc++_shared") {
|
||||
@@ -3352,13 +3523,9 @@ impl BuildRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the platforms that are enabled for the package
|
||||
///
|
||||
/// Ideally only one platform is enabled but we need to be able to
|
||||
pub(crate) fn enabled_cargo_toml_renderers(
|
||||
pub(crate) fn renderer_enabled_by_dioxus_dependency(
|
||||
package: &krates::cm::Package,
|
||||
no_default_features: bool,
|
||||
) -> Vec<(Renderer, String)> {
|
||||
) -> Option<(Renderer, String)> {
|
||||
let mut renderers = vec![];
|
||||
|
||||
// Attempt to discover the platform directly from the dioxus dependency
|
||||
@@ -3367,13 +3534,40 @@ impl BuildRequest {
|
||||
// dioxus = { features = ["web"] }
|
||||
//
|
||||
if let Some(dxs) = package.dependencies.iter().find(|dep| dep.name == "dioxus") {
|
||||
for f in dxs.features.iter() {
|
||||
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(f) {
|
||||
renderers.push((renderer, f.clone()));
|
||||
for feature in dxs.features.iter() {
|
||||
if let Some(renderer) = Renderer::autodetect_from_cargo_feature(feature) {
|
||||
renderers.push((renderer, format!("dioxus/{}", feature)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if renderers.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(renderers[0].clone())
|
||||
}
|
||||
|
||||
pub(crate) fn features_that_enable_renderers(
|
||||
package: &krates::cm::Package,
|
||||
) -> Vec<(Renderer, String)> {
|
||||
package
|
||||
.features
|
||||
.keys()
|
||||
.filter_map(|key| {
|
||||
Renderer::autodetect_from_cargo_feature(key).map(|v| (v, key.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the platforms that are enabled for the package only from the default features
|
||||
///
|
||||
/// Ideally only one platform is enabled but we need to be able to
|
||||
pub(crate) fn enabled_cargo_toml_default_features_renderers(
|
||||
package: &krates::cm::Package,
|
||||
) -> Vec<(Renderer, String)> {
|
||||
let mut renderers = vec![];
|
||||
|
||||
// Start searching through the default features
|
||||
//
|
||||
// [features]
|
||||
@@ -3384,10 +3578,6 @@ impl BuildRequest {
|
||||
// [features]
|
||||
// default = ["web"]
|
||||
// web = ["dioxus/web"]
|
||||
if no_default_features {
|
||||
return renderers;
|
||||
}
|
||||
|
||||
let Some(default) = package.features.get("default") else {
|
||||
return renderers;
|
||||
};
|
||||
@@ -4396,7 +4586,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
///
|
||||
/// This might stop working if/when cargo stabilizes contents-based fingerprinting.
|
||||
fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> {
|
||||
if matches!(ctx.mode, BuildMode::Fat | BuildMode::Base { run: true }) {
|
||||
if matches!(ctx.mode, BuildMode::Fat) {
|
||||
// `dx` compiles everything with `--target` which ends up with a structure like:
|
||||
// target/<triple>/<profile>/.fingerprint/<package_name>-<hash>
|
||||
//
|
||||
|
||||
@@ -3,8 +3,7 @@ use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use target_lexicon::{
|
||||
Aarch64Architecture, Architecture, ArmArchitecture, Environment, OperatingSystem, Triple,
|
||||
X86_32Architecture,
|
||||
Aarch64Architecture, Architecture, ArmArchitecture, Triple, X86_32Architecture,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
@@ -236,9 +235,6 @@ impl AndroidTools {
|
||||
// - We try to match the architecture unless otherwise specified. This is because
|
||||
// emulators that match the host arch are usually faster.
|
||||
let mut triple = "aarch64-linux-android".parse::<Triple>().unwrap();
|
||||
triple.operating_system = OperatingSystem::Linux;
|
||||
triple.environment = Environment::Android;
|
||||
triple.architecture = target_lexicon::HOST.architecture;
|
||||
|
||||
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
|
||||
let output = Command::new(&self.adb)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use dioxus_dx_wire_format::StructuredBuildArtifacts;
|
||||
|
||||
use crate::{
|
||||
cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, TargetArgs, Workspace,
|
||||
cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, BundleFormat,
|
||||
TargetArgs, Workspace,
|
||||
};
|
||||
|
||||
/// Build the Rust Dioxus app and all of its assets.
|
||||
@@ -116,14 +117,22 @@ impl CommandWithPlatformOverrides<BuildArgs> {
|
||||
Some(server_args) => {
|
||||
// Make sure we set the client target here so @server knows to place its output into the @client target directory.
|
||||
server_args.build_arguments.client_target = Some(client.main_target.clone());
|
||||
|
||||
// We don't override anything except the bundle format since @server usually implies a server output
|
||||
server_args.build_arguments.bundle = server_args
|
||||
.build_arguments
|
||||
.bundle
|
||||
.or(Some(BundleFormat::Server));
|
||||
|
||||
server = Some(
|
||||
BuildRequest::new(&server_args.build_arguments, workspace.clone()).await?,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let mut args = self.shared.build_arguments.clone();
|
||||
args.platform = Some(crate::Platform::Server);
|
||||
args.renderer.renderer = Some(crate::Renderer::Server);
|
||||
args.platform = crate::Platform::Server;
|
||||
args.renderer = Some(crate::Renderer::Server);
|
||||
args.bundle = Some(crate::BundleFormat::Server);
|
||||
args.target = Some(target_lexicon::Triple::host());
|
||||
server = Some(BuildRequest::new(&args, workspace.clone()).await?);
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ impl LinkAction {
|
||||
target_lexicon::BinaryFormat::Macho => object::BinaryFormat::MachO,
|
||||
target_lexicon::BinaryFormat::Wasm => object::BinaryFormat::Wasm,
|
||||
target_lexicon::BinaryFormat::Xcoff => object::BinaryFormat::Xcoff,
|
||||
target_lexicon::BinaryFormat::Unknown => todo!(),
|
||||
_ => todo!("Binary format not supported"),
|
||||
target_lexicon::BinaryFormat::Unknown => unimplemented!(),
|
||||
_ => unimplemented!("Binary format not supported"),
|
||||
};
|
||||
|
||||
let arch = match triple.architecture {
|
||||
@@ -202,13 +202,13 @@ impl LinkAction {
|
||||
target_lexicon::Architecture::Aarch64(_) => object::Architecture::Aarch64,
|
||||
target_lexicon::Architecture::LoongArch64 => object::Architecture::LoongArch64,
|
||||
target_lexicon::Architecture::Unknown => object::Architecture::Unknown,
|
||||
_ => todo!("Architecture not supported"),
|
||||
_ => unimplemented!("Architecture not supported"),
|
||||
};
|
||||
|
||||
let endian = match triple.endianness() {
|
||||
Ok(target_lexicon::Endianness::Little) => object::Endianness::Little,
|
||||
Ok(target_lexicon::Endianness::Big) => object::Endianness::Big,
|
||||
Err(_) => todo!("Endianness not supported"),
|
||||
Err(_) => unimplemented!("Endianness not supported"),
|
||||
};
|
||||
|
||||
let bytes = object::write::Object::new(format, arch, endian)
|
||||
|
||||
@@ -102,7 +102,9 @@ impl RunArgs {
|
||||
path,
|
||||
} => tracing::info!(
|
||||
"[{bundle_format}] Copying asset {} ({current}/{total})",
|
||||
path.display(),
|
||||
path.file_name()
|
||||
.map(|f| f.to_string_lossy())
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
BuildStage::Bundling => {
|
||||
tracing::info!("[{bundle_format}] Bundling app")
|
||||
|
||||
@@ -61,7 +61,7 @@ pub(crate) struct ServeArgs {
|
||||
pub(crate) hot_patch: bool,
|
||||
|
||||
/// Watch the filesystem for changes and trigger a rebuild [default: true]
|
||||
#[clap(long, default_missing_value = "true")]
|
||||
#[clap(long, default_missing_value = "true", num_args=0..=1)]
|
||||
pub(crate) watch: Option<bool>,
|
||||
|
||||
/// Exit the CLI after running into an error. This is mainly used to test hot patching internally
|
||||
@@ -92,10 +92,6 @@ impl ServeArgs {
|
||||
///
|
||||
/// We also set up proper panic handling since the TUI has a tendency to corrupt the terminal.
|
||||
pub(crate) async fn serve(self, tracer: &TraceController) -> Result<StructuredOutput> {
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
// Redirect all logging the cli logger - if there's any pending after a panic, we flush it
|
||||
let is_interactive_tty = self.is_interactive_tty();
|
||||
if is_interactive_tty {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::cli::*;
|
||||
use crate::BundleFormat;
|
||||
use crate::Platform;
|
||||
use crate::RendererArg;
|
||||
use crate::TargetAlias;
|
||||
use crate::{cli::*, Renderer};
|
||||
// use crate::RendererArg;
|
||||
// use crate::PlatformAlias;
|
||||
use target_lexicon::Triple;
|
||||
|
||||
const HELP_HEADING: &str = "Target Options";
|
||||
@@ -10,18 +10,6 @@ const HELP_HEADING: &str = "Target Options";
|
||||
/// A single target to build for
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub(crate) struct TargetArgs {
|
||||
/// The target alias to use for this build. Supports wasm, macos, windows, linux, ios, android, and host [default: "host"]
|
||||
#[clap(flatten)]
|
||||
pub(crate) target_alias: TargetAlias,
|
||||
|
||||
/// Build renderer: supports web, webview, native, server, and liveview
|
||||
#[clap(flatten)]
|
||||
pub(crate) renderer: RendererArg,
|
||||
|
||||
/// The bundle format to target for the build: supports web, macos, windows, linux, ios, android, and server
|
||||
#[clap(long, value_enum, help_heading = HELP_HEADING)]
|
||||
pub(crate) bundle: Option<BundleFormat>,
|
||||
|
||||
/// Build platform: supports Web, MacOS, Windows, Linux, iOS, Android, and Server
|
||||
///
|
||||
/// The platform implies a combination of the target alias, renderer, and bundle format flags.
|
||||
@@ -29,8 +17,16 @@ pub(crate) struct TargetArgs {
|
||||
/// You should generally prefer to use the `--web`, `--webview`, or `--native` flags to set the renderer
|
||||
/// or the `--wasm`, `--macos`, `--windows`, `--linux`, `--ios`, or `--android` flags to set the target alias
|
||||
/// instead of this flag. The renderer, target alias, and bundle format will be inferred if you only pass one.
|
||||
#[clap(flatten)]
|
||||
pub(crate) platform: Platform,
|
||||
|
||||
/// Which renderer to use? By default, this is usually inferred from the platform.
|
||||
#[clap(long, value_enum, help_heading = HELP_HEADING)]
|
||||
pub(crate) platform: Option<Platform>,
|
||||
pub(crate) renderer: Option<Renderer>,
|
||||
|
||||
/// The bundle format to target for the build: supports web, macos, windows, linux, ios, android, and server
|
||||
#[clap(long, value_enum, help_heading = HELP_HEADING)]
|
||||
pub(crate) bundle: Option<BundleFormat>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long, short, help_heading = HELP_HEADING)]
|
||||
@@ -112,7 +108,7 @@ pub(crate) struct TargetArgs {
|
||||
/// simulator. If the device name is passed, we will upload to that device instead.
|
||||
///
|
||||
/// This performs a search among devices, and fuzzy matches might be found.
|
||||
#[arg(long, default_missing_value=None, num_args=0..=1, help_heading = HELP_HEADING)]
|
||||
#[arg(long, default_missing_value=Some("".into()), num_args=0..=1)]
|
||||
pub(crate) device: Option<String>,
|
||||
|
||||
/// The base path the build will fetch assets relative to. This will override the
|
||||
@@ -153,12 +149,15 @@ pub(crate) struct TargetArgs {
|
||||
/// when merging `@client and @server` targets together.
|
||||
#[clap(long, help_heading = HELP_HEADING)]
|
||||
pub(crate) client_target: Option<String>,
|
||||
|
||||
/// Automatically pass `--features=js_cfg` when building for wasm targets. This is enabled by default.
|
||||
#[clap(long, default_value_t = true, help_heading = HELP_HEADING, num_args = 0..=1)]
|
||||
pub(crate) wasm_js_cfg: bool,
|
||||
}
|
||||
|
||||
impl Anonymized for TargetArgs {
|
||||
fn anonymized(&self) -> Value {
|
||||
json! {{
|
||||
"target_alias": self.target_alias,
|
||||
"renderer": self.renderer,
|
||||
"bundle": self.bundle,
|
||||
"platform": self.platform,
|
||||
|
||||
@@ -18,6 +18,7 @@ mod rustcwrapper;
|
||||
mod serve;
|
||||
mod settings;
|
||||
mod tailwind;
|
||||
mod test_harnesses;
|
||||
mod wasm_bindgen;
|
||||
mod wasm_opt;
|
||||
mod workspace;
|
||||
|
||||
+119
-334
@@ -1,82 +1,106 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use clap::{arg, ArgMatches, Args, FromArgMatches};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use target_lexicon::{Architecture, Environment, OperatingSystem, Triple};
|
||||
|
||||
use crate::Workspace;
|
||||
use target_lexicon::{Environment, OperatingSystem, Triple};
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug, Default,
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub(crate) enum TargetAlias {
|
||||
/// Targeting the WASM architecture
|
||||
Wasm,
|
||||
pub(crate) enum Platform {
|
||||
/// Alias for `--target wasm32-unknown-unknown --renderer websys --bundle-format web`
|
||||
#[serde(rename = "web")]
|
||||
Web,
|
||||
|
||||
/// Targeting macos desktop
|
||||
/// Alias for `--target <host> --renderer webview --bundle-format macos`
|
||||
#[serde(rename = "macos")]
|
||||
MacOS,
|
||||
|
||||
/// Targeting windows desktop
|
||||
/// Alias for `--target <host> --renderer webview --bundle-format windows`
|
||||
#[serde(rename = "windows")]
|
||||
Windows,
|
||||
|
||||
/// Targeting linux desktop
|
||||
/// Alias for `--target <host> --renderer webview --bundle-format linux`
|
||||
#[serde(rename = "linux")]
|
||||
Linux,
|
||||
|
||||
/// Targeting the ios platform
|
||||
///
|
||||
/// Can't work properly if you're not building from an Apple device.
|
||||
/// Alias for `--target <aarch64-apple-ios/sim> --renderer webview --bundle-format ios`
|
||||
#[serde(rename = "ios")]
|
||||
Ios,
|
||||
|
||||
/// Targeting the android platform
|
||||
/// Alias for `--target <device-triple> --renderer webview --bundle-format android`
|
||||
#[serde(rename = "android")]
|
||||
Android,
|
||||
|
||||
/// Targeting the current target
|
||||
Host,
|
||||
/// Alias for `--target <host> --renderer ssr --bundle-format server`
|
||||
#[serde(rename = "server")]
|
||||
Server,
|
||||
|
||||
/// An unknown target platform
|
||||
/// Alias for `--target <host> --renderer liveview --bundle-format server`
|
||||
#[serde(rename = "liveview")]
|
||||
Liveview,
|
||||
|
||||
/// No platform was specified, so the CLI is free to choose the best one.
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Args for TargetAlias {
|
||||
impl Args for Platform {
|
||||
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
|
||||
fn augment_args(cmd: clap::Command) -> clap::Command {
|
||||
const HELP_HEADING: &str = "Target Alias";
|
||||
cmd.arg(arg!(--wasm "Target the wasm triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--macos "Target the macos triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--windows "Target the windows triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--linux "Target the linux triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--ios "Target the ios triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--android "Target the android triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--host "Target the host triple").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--desktop "Target the host triple").help_heading(HELP_HEADING))
|
||||
const HELP_HEADING: &str = "Platform";
|
||||
cmd.arg(arg!(--web "Target a web app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--desktop "Target a desktop app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--macos "Target a macos desktop app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--windows "Target a windows desktop app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--linux "Target a linux desktop app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--ios "Target an ios app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--android "Target an android app").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--server "Target a server build").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--liveview "Target a liveview build").help_heading(HELP_HEADING))
|
||||
.group(
|
||||
clap::ArgGroup::new("target_alias")
|
||||
.args([
|
||||
"wasm", "macos", "windows", "linux", "ios", "android", "host", "host",
|
||||
"web", "desktop", "macos", "windows", "linux", "ios", "android", "server",
|
||||
"liveview",
|
||||
])
|
||||
.multiple(false)
|
||||
.required(false),
|
||||
)
|
||||
}
|
||||
|
||||
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArgMatches for TargetAlias {
|
||||
impl FromArgMatches for Platform {
|
||||
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
|
||||
if let Some(platform) = matches.get_one::<clap::Id>("target_alias") {
|
||||
match platform.as_str() {
|
||||
"wasm" => Ok(Self::Wasm),
|
||||
"web" => Ok(Self::Web),
|
||||
"desktop" => {
|
||||
if cfg!(target_os = "macos") {
|
||||
Ok(Self::MacOS)
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Ok(Self::Windows)
|
||||
} else if cfg!(unix) {
|
||||
Ok(Self::Linux)
|
||||
} else {
|
||||
Err(clap::Error::raw(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
"Desktop alias is not supported on this platform",
|
||||
))
|
||||
}
|
||||
}
|
||||
"macos" => Ok(Self::MacOS),
|
||||
"windows" => Ok(Self::Windows),
|
||||
"linux" => Ok(Self::Linux),
|
||||
"ios" => Ok(Self::Ios),
|
||||
"android" => Ok(Self::Android),
|
||||
"host" => Ok(Self::Host),
|
||||
"liveview" => Ok(Self::Liveview),
|
||||
"server" => Ok(Self::Server),
|
||||
_ => Err(clap::Error::raw(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
format!("Unknown target alias: {platform}"),
|
||||
@@ -86,134 +110,26 @@ impl FromArgMatches for TargetAlias {
|
||||
Ok(Self::Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
|
||||
*self = Self::from_arg_matches(matches)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TargetAlias {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::MacOS);
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Windows);
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Linux);
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = None;
|
||||
|
||||
pub(crate) async fn into_target(self, device: bool, workspace: &Workspace) -> Result<Triple> {
|
||||
match self {
|
||||
// Generally just use the host's triple for native executables unless specified otherwise
|
||||
Self::MacOS | Self::Windows | Self::Linux | Self::Host | Self::Unknown => {
|
||||
Ok(Triple::host())
|
||||
}
|
||||
|
||||
// We currently assume unknown-unknown for web, but we might want to eventually
|
||||
// support emscripten
|
||||
Self::Wasm => Ok("wasm32-unknown-unknown".parse()?),
|
||||
|
||||
// For iOS we should prefer the actual architecture for the simulator, but in lieu of actually
|
||||
// figuring that out, we'll assume aarch64 on m-series and x86_64 otherwise
|
||||
Self::Ios => {
|
||||
// use the host's architecture and sim if --device is passed
|
||||
use target_lexicon::{Architecture, HOST};
|
||||
let triple_str = match HOST.architecture {
|
||||
Architecture::Aarch64(_) if device => "aarch64-apple-ios",
|
||||
Architecture::Aarch64(_) => "aarch64-apple-ios-sim",
|
||||
_ if device => "x86_64-apple-ios",
|
||||
_ => "x86_64-apple-ios",
|
||||
};
|
||||
Ok(triple_str.parse()?)
|
||||
}
|
||||
|
||||
// Same idea with android but we figure out the connected device using adb
|
||||
Self::Android => Ok(workspace
|
||||
.android_tools()?
|
||||
.autodetect_android_device_triple()
|
||||
.await),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn or(self, other: Self) -> Self {
|
||||
if self == Self::Unknown {
|
||||
other
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug, Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
clap::ValueEnum,
|
||||
)]
|
||||
pub(crate) struct RendererArg {
|
||||
pub(crate) renderer: Option<Renderer>,
|
||||
}
|
||||
|
||||
impl Args for RendererArg {
|
||||
fn augment_args(cmd: clap::Command) -> clap::Command {
|
||||
const HELP_HEADING: &str = "Renderer";
|
||||
cmd.arg(arg!(--web "Enable the dioxus web renderer").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--webview "Enable the dioxus webview renderer").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--native "Enable the dioxus native renderer").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--server "Enable the dioxus server renderer").help_heading(HELP_HEADING))
|
||||
.arg(arg!(--liveview "Enable the dioxus liveview renderer").help_heading(HELP_HEADING))
|
||||
.group(
|
||||
clap::ArgGroup::new("renderer")
|
||||
.args(["web", "webview", "native", "server", "liveview"])
|
||||
.multiple(false)
|
||||
.required(false),
|
||||
)
|
||||
}
|
||||
|
||||
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArgMatches for RendererArg {
|
||||
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
|
||||
if let Some(renderer) = matches.get_one::<clap::Id>("renderer") {
|
||||
match renderer.as_str() {
|
||||
"web" => Ok(Self {
|
||||
renderer: Some(Renderer::Web),
|
||||
}),
|
||||
"webview" => Ok(Self {
|
||||
renderer: Some(Renderer::Webview),
|
||||
}),
|
||||
"native" => Ok(Self {
|
||||
renderer: Some(Renderer::Native),
|
||||
}),
|
||||
"server" => Ok(Self {
|
||||
renderer: Some(Renderer::Server),
|
||||
}),
|
||||
"liveview" => Ok(Self {
|
||||
renderer: Some(Renderer::Liveview),
|
||||
}),
|
||||
_ => Err(clap::Error::raw(
|
||||
clap::error::ErrorKind::InvalidValue,
|
||||
format!("Unknown platform: {renderer}"),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Ok(Self { renderer: None })
|
||||
}
|
||||
}
|
||||
|
||||
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), clap::Error> {
|
||||
*self = Self::from_arg_matches(matches)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RendererArg> for Option<Renderer> {
|
||||
fn from(val: RendererArg) -> Self {
|
||||
val.renderer
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub(crate) enum Renderer {
|
||||
/// Targeting webview renderer
|
||||
@@ -231,7 +147,7 @@ pub(crate) enum Renderer {
|
||||
/// Targeting the static generation platform using SSR and Dioxus-Fullstack
|
||||
Liveview,
|
||||
|
||||
/// Targeting the web renderer
|
||||
/// Targeting the web-sys renderer
|
||||
Web,
|
||||
}
|
||||
|
||||
@@ -260,24 +176,6 @@ impl Renderer {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn default_platform(&self) -> TargetAlias {
|
||||
match self {
|
||||
Renderer::Webview | Renderer::Native | Renderer::Server | Renderer::Liveview => {
|
||||
TargetAlias::TARGET_PLATFORM.unwrap()
|
||||
}
|
||||
Renderer::Web => TargetAlias::Wasm,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_target(triple: &Triple) -> Self {
|
||||
match triple.architecture {
|
||||
// Assume any wasm32 or wasm64 target is a web target
|
||||
Architecture::Wasm32 | Architecture::Wasm64 => Self::Web,
|
||||
// Otherwise, assume webview for native targets
|
||||
_ => Self::Webview,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -357,57 +255,19 @@ pub(crate) enum BundleFormat {
|
||||
Android,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnknownBundleFormatError;
|
||||
|
||||
impl std::error::Error for UnknownBundleFormatError {}
|
||||
|
||||
impl std::fmt::Display for UnknownBundleFormatError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Unknown bundle format")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for BundleFormat {
|
||||
type Err = UnknownBundleFormatError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"web" => Ok(Self::Web),
|
||||
"macos" => Ok(Self::MacOS),
|
||||
"windows" => Ok(Self::Windows),
|
||||
"linux" => Ok(Self::Linux),
|
||||
"server" => Ok(Self::Server),
|
||||
"ios" => Ok(Self::Ios),
|
||||
"android" => Ok(Self::Android),
|
||||
_ => Err(UnknownBundleFormatError),
|
||||
impl BundleFormat {
|
||||
/// The native "desktop" host app format.
|
||||
pub(crate) fn host() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Self::MacOS
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else if cfg!(target_os = "linux") {
|
||||
Self::Linux
|
||||
} else {
|
||||
Self::Web
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BundleFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
BundleFormat::Web => "web",
|
||||
BundleFormat::MacOS => "macos",
|
||||
BundleFormat::Windows => "windows",
|
||||
BundleFormat::Linux => "linux",
|
||||
BundleFormat::Server => "server",
|
||||
BundleFormat::Ios => "ios",
|
||||
BundleFormat::Android => "android",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BundleFormat {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::MacOS);
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Windows);
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = Some(Self::Linux);
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||
pub(crate) const TARGET_PLATFORM: Option<Self> = None;
|
||||
|
||||
/// Get the name of the folder we need to generate for this platform
|
||||
///
|
||||
@@ -449,121 +309,46 @@ impl BundleFormat {
|
||||
Self::Server => "Server",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_target(target: &Triple, renderer: Option<Renderer>) -> Result<BundleFormat> {
|
||||
match (
|
||||
renderer,
|
||||
target.architecture,
|
||||
target.environment,
|
||||
target.operating_system,
|
||||
) {
|
||||
// The server always uses the server bundle format
|
||||
(Some(Renderer::Server), _, _, _) => Ok(BundleFormat::Server),
|
||||
// The web renderer always uses the web bundle format
|
||||
(Some(Renderer::Web), _, _, _) => Ok(BundleFormat::Web),
|
||||
// Otherwise, guess it based on the target
|
||||
// Assume any wasm32 or wasm64 target is a web target
|
||||
(_, Architecture::Wasm32 | Architecture::Wasm64, _, _) => Ok(BundleFormat::Web),
|
||||
// For native targets, we need to determine the bundle format based on the OS
|
||||
(_, _, Environment::Android, _) => Ok(BundleFormat::Android),
|
||||
(_, _, _, OperatingSystem::IOS(_)) => Ok(BundleFormat::Ios),
|
||||
(_, _, _, OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_)) => {
|
||||
Ok(BundleFormat::MacOS)
|
||||
}
|
||||
(_, _, _, OperatingSystem::Linux) => Ok(BundleFormat::Linux),
|
||||
(_, _, _, OperatingSystem::Windows) => Ok(BundleFormat::Windows),
|
||||
// If we don't recognize the target, default to desktop
|
||||
_ => BundleFormat::TARGET_PLATFORM.context(
|
||||
"failed to determine bundle format. Try setting the `--bundle` flag manually",
|
||||
),
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnknownBundleFormatError;
|
||||
|
||||
impl std::error::Error for UnknownBundleFormatError {}
|
||||
|
||||
impl std::fmt::Display for UnknownBundleFormatError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Unknown bundle format")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for BundleFormat {
|
||||
type Err = UnknownBundleFormatError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"web" => Ok(Self::Web),
|
||||
"macos" => Ok(Self::MacOS),
|
||||
"windows" => Ok(Self::Windows),
|
||||
"linux" => Ok(Self::Linux),
|
||||
"server" => Ok(Self::Server),
|
||||
"ios" => Ok(Self::Ios),
|
||||
"android" => Ok(Self::Android),
|
||||
_ => Err(UnknownBundleFormatError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug,
|
||||
Default,
|
||||
clap::ValueEnum,
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub(crate) enum Platform {
|
||||
/// Alias for `--wasm --web --bundle-format web`
|
||||
#[clap(name = "web")]
|
||||
#[serde(rename = "web")]
|
||||
#[default]
|
||||
Web,
|
||||
|
||||
/// Alias for `--macos --webview --bundle-format macos`
|
||||
#[cfg_attr(target_os = "macos", clap(alias = "desktop"))]
|
||||
#[clap(name = "macos")]
|
||||
#[serde(rename = "macos")]
|
||||
MacOS,
|
||||
|
||||
/// Alias for `--windows --webview --bundle-format windows`
|
||||
#[cfg_attr(target_os = "windows", clap(alias = "desktop"))]
|
||||
#[clap(name = "windows")]
|
||||
#[serde(rename = "windows")]
|
||||
Windows,
|
||||
|
||||
/// Alias for `--linux --webview --bundle-format linux`
|
||||
#[cfg_attr(target_os = "linux", clap(alias = "desktop"))]
|
||||
#[clap(name = "linux")]
|
||||
#[serde(rename = "linux")]
|
||||
Linux,
|
||||
|
||||
/// Alias for `--ios --webview --bundle-format ios`
|
||||
#[clap(name = "ios")]
|
||||
#[serde(rename = "ios")]
|
||||
Ios,
|
||||
|
||||
/// Alias for `--android --webview --bundle-format android`
|
||||
#[clap(name = "android")]
|
||||
#[serde(rename = "android")]
|
||||
Android,
|
||||
|
||||
/// Alias for `--host --server --bundle-format server`
|
||||
#[clap(name = "server")]
|
||||
#[serde(rename = "server")]
|
||||
Server,
|
||||
|
||||
/// Alias for `--host --liveview --bundle-format host`
|
||||
#[clap(name = "liveview")]
|
||||
#[serde(rename = "liveview")]
|
||||
Liveview,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
pub(crate) fn into_triple(self) -> (TargetAlias, Renderer, BundleFormat) {
|
||||
match self {
|
||||
Platform::Web => (TargetAlias::Wasm, Renderer::Web, BundleFormat::Web),
|
||||
Platform::MacOS => (TargetAlias::MacOS, Renderer::Webview, BundleFormat::MacOS),
|
||||
Platform::Windows => (
|
||||
TargetAlias::Windows,
|
||||
Renderer::Webview,
|
||||
BundleFormat::Windows,
|
||||
),
|
||||
Platform::Linux => (TargetAlias::Linux, Renderer::Webview, BundleFormat::Linux),
|
||||
Platform::Ios => (TargetAlias::Ios, Renderer::Webview, BundleFormat::Ios),
|
||||
Platform::Android => (
|
||||
TargetAlias::Android,
|
||||
Renderer::Webview,
|
||||
BundleFormat::Android,
|
||||
),
|
||||
Platform::Server => (TargetAlias::Host, Renderer::Server, BundleFormat::Server),
|
||||
Platform::Liveview => (
|
||||
TargetAlias::Host,
|
||||
Renderer::Liveview,
|
||||
BundleFormat::TARGET_PLATFORM.unwrap(),
|
||||
),
|
||||
}
|
||||
impl Display for BundleFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
BundleFormat::Web => "web",
|
||||
BundleFormat::MacOS => "macos",
|
||||
BundleFormat::Windows => "windows",
|
||||
BundleFormat::Linux => "linux",
|
||||
BundleFormat::Server => "server",
|
||||
BundleFormat::Ios => "ios",
|
||||
BundleFormat::Android => "android",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,13 @@ pub(crate) async fn proxy_websocket(
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
|
||||
tracing::info!(dx_src = ?TraceSrc::Dev, "Proxying websocket connection {req:?}");
|
||||
tracing::trace!(dx_src = ?TraceSrc::Dev, "Proxying websocket connection {req:?}");
|
||||
let proxied_request = into_proxied_request(req, backend_url).map_err(handle_proxy_error)?;
|
||||
tracing::info!(dx_src = ?TraceSrc::Dev, "Connection proxied to {proxied_uri}", proxied_uri = proxied_request.uri());
|
||||
tracing::trace!(dx_src = ?TraceSrc::Dev, "Connection proxied to {proxied_uri}", proxied_uri = proxied_request.uri());
|
||||
|
||||
Ok(ws.on_upgrade(move |client_ws| async move {
|
||||
match handle_ws_connection(client_ws, proxied_request).await {
|
||||
Ok(()) => tracing::info!(dx_src = ?TraceSrc::Dev, "Websocket connection closed"),
|
||||
Ok(()) => tracing::trace!(dx_src = ?TraceSrc::Dev, "Websocket connection closed"),
|
||||
Err(e) => {
|
||||
tracing::error!(dx_src = ?TraceSrc::Dev, "Error proxying websocket connection: {e}")
|
||||
}
|
||||
|
||||
@@ -337,10 +337,6 @@ impl WebServer {
|
||||
|
||||
/// Tells all clients to reload if possible for new changes.
|
||||
pub(crate) async fn send_reload_command(&mut self) {
|
||||
tracing::trace!("Sending reload to toast");
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
self.build_status.set(Status::Ready);
|
||||
self.send_build_status().await;
|
||||
self.send_devserver_message_to_all(DevserverMsg::FullReloadCommand)
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
use crate::{BuildTargets, BundleFormat, Cli, Commands, Workspace};
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
prelude::rust_2024::Future,
|
||||
};
|
||||
use target_lexicon::Triple;
|
||||
use tracing_subscriber::{prelude::*, util::SubscriberInitExt, EnvFilter, Layer};
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_harness() {
|
||||
test_harnesses().await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn test_harnesses() {
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer().with_filter(EnvFilter::new("error,dx=debug,dioxus_cli=debug,manganis_cli_support=debug,wasm_split_cli=debug,subsecond_cli_support=debug",)))
|
||||
.init();
|
||||
|
||||
TestHarnessBuilder::run(vec![
|
||||
TestHarnessBuilder::new("harness-simple-web")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["web"] }"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
assert_eq!(t.client.triple, "wasm32-unknown-unknown".parse().unwrap());
|
||||
assert!(t.server.is_none());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-simple-desktop")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["desktop"] }"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
assert_eq!(t.client.triple, Triple::host());
|
||||
assert!(t.server.is_none());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-simple-mobile")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["mobile"] }"#)
|
||||
.asrt(
|
||||
"dx build",
|
||||
|targets| async move { assert!(targets.is_err()) },
|
||||
),
|
||||
TestHarnessBuilder::new("harness-simple-fullstack")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"web=["dioxus/web"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-simple-fullstack-with-default")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"default=["web", "server"]"#)
|
||||
.fetr(r#"web=["dioxus/web"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-simple-fullstack-native-with-default")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"default=["native", "server"]"#)
|
||||
.fetr(r#"native=["dioxus/native"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
assert_eq!(t.client.features.len(), 1);
|
||||
assert_eq!(t.client.features[0], "native");
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-fullstack-multi-target")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"default=["web", "desktop", "mobile", "server"]"#)
|
||||
.fetr(r#"web=["dioxus/web"]"#)
|
||||
.fetr(r#"desktop=["dioxus/desktop"]"#)
|
||||
.fetr(r#"mobile=["dioxus/mobile"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |t| async move { assert!(t.is_err()) })
|
||||
.asrt(r#"dx build --web"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
})
|
||||
.asrt(r#"dx build --desktop"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
})
|
||||
.asrt(r#"dx build --ios"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Ios);
|
||||
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
|
||||
})
|
||||
.asrt(r#"dx build --ios --device"#, |targets| async move {
|
||||
let targets = targets.unwrap();
|
||||
assert_eq!(targets.client.bundle, BundleFormat::Ios);
|
||||
assert_eq!(targets.client.triple, "aarch64-apple-ios".parse().unwrap());
|
||||
})
|
||||
.asrt(r#"dx build --android --device"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Android);
|
||||
assert_eq!(t.client.triple, "aarch64-linux-android".parse().unwrap());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-fullstack-multi-target-no-default")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"web=["dioxus/web"]"#)
|
||||
.fetr(r#"desktop=["dioxus/desktop"]"#)
|
||||
.fetr(r#"mobile=["dioxus/mobile"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
assert!(targets.is_err())
|
||||
})
|
||||
.asrt(r#"dx build --desktop"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
})
|
||||
.asrt(r#"dx build --ios"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Ios);
|
||||
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-fullstack-desktop")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"desktop=["dioxus/desktop"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-fullstack-desktop-with-features")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.deps(r#"anyhow = { workspace = true, optional = true }"#)
|
||||
.fetr(r#"desktop=["dioxus/desktop", "has_anyhow"]"#)
|
||||
.fetr(r#"has_anyhow=["dep:anyhow"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-fullstack-desktop-with-default")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.deps(r#"anyhow = { workspace = true, optional = true }"#)
|
||||
.fetr(r#"default=["desktop"]"#)
|
||||
.fetr(r#"desktop=["dioxus/desktop", "has_anyhow"]"#)
|
||||
.fetr(r#"has_anyhow=["dep:anyhow"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-no-dioxus")
|
||||
.deps(r#"anyhow = { workspace = true, optional = true }"#)
|
||||
.fetr(r#"web=["dep:anyhow"]"#)
|
||||
.fetr(r#"server=[]"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
assert!(t.server.is_none());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-simple-dedicated-server"),
|
||||
TestHarnessBuilder::new("harness-simple-dedicated-client")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["web"] }"#)
|
||||
.asrt(r#"dx build"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
assert!(t.server.is_none());
|
||||
})
|
||||
.asrt(r#"dx build @client --package harness-simple-dedicated-client @server --package harness-simple-dedicated-server"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
let s = t.server.unwrap();
|
||||
assert_eq!(s.bundle, BundleFormat::Server);
|
||||
assert_eq!(s.triple, Triple::host());
|
||||
},
|
||||
)
|
||||
.asrt(r#"dx build @client --package harness-simple-dedicated-client @server --package harness-simple-dedicated-server --target wasm32-unknown-unknown"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
let s = t.server.unwrap();
|
||||
assert_eq!(s.bundle, BundleFormat::Server);
|
||||
assert_eq!(s.triple, "wasm32-unknown-unknown".parse().unwrap());
|
||||
}),
|
||||
TestHarnessBuilder::new("harness-renderer-swap")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.fetr(r#"default=["desktop", "server"]"#)
|
||||
.fetr(r#"desktop=["dioxus/desktop"]"#)
|
||||
.fetr(r#"native=["dioxus/native"]"#)
|
||||
.fetr(r#"server=["dioxus/server"]"#)
|
||||
.asrt(
|
||||
r#"dx build --desktop --renderer native"#,
|
||||
|targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::host());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
},
|
||||
),
|
||||
TestHarnessBuilder::new("harness-default-to-non-default")
|
||||
.deps(r#"dioxus = { workspace = true, features = [] }"#)
|
||||
.fetr(r#"default=["web"]"#)
|
||||
.fetr(r#"web=["dioxus/web"]"#)
|
||||
.asrt(
|
||||
r#"dx build --ios"#,
|
||||
|targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert!(t.server.is_none());
|
||||
assert_eq!(t.client.bundle, BundleFormat::Ios);
|
||||
assert_eq!(t.client.triple, TestHarnessBuilder::host_ios_triple_sim());
|
||||
assert!(t.client.no_default_features);
|
||||
},
|
||||
),
|
||||
TestHarnessBuilder::new("harness-fullstack-with-optional-tokio")
|
||||
.deps(r#"dioxus = { workspace = true, features = ["fullstack"] }"#)
|
||||
.deps(r#"serde = "1.0.219""#)
|
||||
.deps(r#"tokio = { workspace = true, features = ["full"], optional = true }"#)
|
||||
.fetr(r#"default = []"#)
|
||||
.fetr(r#"server = ["dioxus/server", "dep:tokio"]"#)
|
||||
.fetr(r#"web = ["dioxus/web"]"#)
|
||||
// .asrt(r#"dx build"#, |targets| async move {
|
||||
// assert!(targets.is_err())
|
||||
// })
|
||||
.asrt(r#"dx build --web"#, |targets| async move {
|
||||
let t = targets.unwrap();
|
||||
assert_eq!(t.client.bundle, BundleFormat::Web);
|
||||
assert_eq!(t.client.triple, "wasm32-unknown-unknown".parse().unwrap());
|
||||
let server = t.server.unwrap();
|
||||
assert_eq!(server.bundle, BundleFormat::Server);
|
||||
assert_eq!(server.triple, Triple::host());
|
||||
})
|
||||
])
|
||||
.await;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestHarnessBuilder {
|
||||
name: String,
|
||||
dependencies: String,
|
||||
features: String,
|
||||
futures: Vec<TestHarnessTestCase>,
|
||||
}
|
||||
|
||||
struct TestHarnessTestCase {
|
||||
args: String,
|
||||
#[allow(clippy::type_complexity)]
|
||||
callback: Box<dyn FnOnce(Result<BuildTargets>) -> Pin<Box<dyn Future<Output = ()>>>>,
|
||||
}
|
||||
|
||||
impl TestHarnessBuilder {
|
||||
fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
dependencies: Default::default(),
|
||||
features: Default::default(),
|
||||
futures: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a dependency to the test harness.
|
||||
fn deps(mut self, dependencies: impl Into<String>) -> Self {
|
||||
writeln!(&mut self.dependencies, "{}", dependencies.into()).unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a feature to the test harness.
|
||||
fn fetr(mut self, features: impl Into<String>) -> Self {
|
||||
writeln!(&mut self.features, "{}", features.into()).unwrap();
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert the expected behavior of the test harness.
|
||||
fn asrt<F>(
|
||||
mut self,
|
||||
args: impl Into<String>,
|
||||
future: impl FnOnce(Result<BuildTargets>) -> F + 'static,
|
||||
) -> Self
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
self.futures.push(TestHarnessTestCase {
|
||||
args: args.into(),
|
||||
callback: Box::new(move |args| Box::pin(future(args))),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Write the test harness to the filesystem.
|
||||
fn build(&self, harness_dir: &Path) {
|
||||
let name = self.name.clone();
|
||||
let dependencies = self.dependencies.clone();
|
||||
let features = self.features.clone();
|
||||
|
||||
let test_dir = harness_dir.join(&name);
|
||||
|
||||
_ = std::fs::remove_dir_all(&test_dir);
|
||||
std::fs::create_dir_all(&test_dir).unwrap();
|
||||
std::fs::create_dir_all(test_dir.join("src")).unwrap();
|
||||
|
||||
let cargo_toml = format!(
|
||||
r#"[package]
|
||||
name = "{name}"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
{dependencies}
|
||||
|
||||
[features]
|
||||
{features}
|
||||
"#,
|
||||
name = name,
|
||||
dependencies = dependencies,
|
||||
features = features
|
||||
);
|
||||
|
||||
std::fs::write(test_dir.join("Cargo.toml"), cargo_toml).unwrap();
|
||||
|
||||
let contents = if features.contains("dioxus") {
|
||||
r#"use dioxus::prelude::*;
|
||||
fn main() {
|
||||
dioxus::launch(|| rsx! { "hello world!" })
|
||||
}
|
||||
"#
|
||||
} else {
|
||||
r#"fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
"#
|
||||
};
|
||||
|
||||
std::fs::write(test_dir.join("src/main.rs"), contents).unwrap();
|
||||
}
|
||||
|
||||
async fn run(harnesses: Vec<Self>) {
|
||||
_ = crate::VERBOSITY.set(crate::Verbosity {
|
||||
verbose: true,
|
||||
trace: true,
|
||||
json_output: false,
|
||||
log_to_file: None,
|
||||
locked: false,
|
||||
offline: false,
|
||||
frozen: false,
|
||||
});
|
||||
|
||||
let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap();
|
||||
|
||||
let harness_dir = cargo_manifest_dir.parent().unwrap().join("cli-harnesses");
|
||||
|
||||
// make sure we don't start deleting random stuff.
|
||||
if !harness_dir.exists() {
|
||||
panic!(
|
||||
"cli-harnesses directory does not exist, aborting: {:?}",
|
||||
harness_dir
|
||||
);
|
||||
}
|
||||
|
||||
// Erase old entries in the harness directory, but keep files (ie README.md) around
|
||||
for entry in std::fs::read_dir(&harness_dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
std::fs::remove_dir_all(entry.path()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Now that the harnesses are written to the filesystem, we can call cargo_metadata
|
||||
// It will be cached from here
|
||||
let mut futures = FuturesUnordered::new();
|
||||
let mut seen_names = HashSet::new();
|
||||
|
||||
for harness in harnesses {
|
||||
if !seen_names.insert(harness.name.clone()) {
|
||||
panic!("Duplicate test harness name found: {}", harness.name);
|
||||
}
|
||||
|
||||
harness.build(&harness_dir);
|
||||
|
||||
for case in harness.futures {
|
||||
let mut escaped = shell_words::split(&case.args).unwrap();
|
||||
if !(escaped.contains(&"--package".to_string())
|
||||
|| escaped.contains(&"@server".to_string())
|
||||
|| escaped.contains(&"@client".to_string()))
|
||||
{
|
||||
escaped.push("--package".to_string());
|
||||
escaped.push(harness.name.clone());
|
||||
}
|
||||
let args = Cli::try_parse_from(escaped).unwrap();
|
||||
let Commands::Build(build_args) = args.action else {
|
||||
panic!("Expected build command");
|
||||
};
|
||||
|
||||
futures.push(async move {
|
||||
let targets = build_args.into_targets().await;
|
||||
(case.callback)(targets).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Give a moment for fs to catch up
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
|
||||
let _workspace = Workspace::current().await.unwrap();
|
||||
|
||||
while let Some(_res) = futures.next().await {}
|
||||
}
|
||||
|
||||
fn host_ios_triple_sim() -> Triple {
|
||||
if cfg!(target_arch = "aarch64") {
|
||||
"aarch64-apple-ios-sim".parse().unwrap()
|
||||
} else {
|
||||
"x86_64-apple-ios".parse().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -855,17 +855,19 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
let ty = f.ty;
|
||||
quote!(#name: #ty)
|
||||
})
|
||||
.chain(self.has_child_owned_fields().then(|| quote!(owner: Owner)));
|
||||
.chain(
|
||||
self.has_child_owned_fields()
|
||||
.then(|| quote!(owner: dioxus_core::internal::generational_box::Owner)),
|
||||
);
|
||||
let global_fields_value = self
|
||||
.extend_fields()
|
||||
.map(|f| {
|
||||
let name = f.extends_vec_ident();
|
||||
quote!(#name: Vec::new())
|
||||
})
|
||||
.chain(
|
||||
self.has_child_owned_fields()
|
||||
.then(|| quote!(owner: Owner::default())),
|
||||
);
|
||||
.chain(self.has_child_owned_fields().then(
|
||||
|| quote!(owner: dioxus_core::internal::generational_box::Owner::default()),
|
||||
));
|
||||
|
||||
Ok(quote! {
|
||||
impl #impl_generics #name #ty_generics #where_clause {
|
||||
|
||||
@@ -25,6 +25,7 @@ warnings = { workspace = true }
|
||||
futures-util = { workspace = true, default-features = false, features = ["alloc", "std"] }
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
subsecond = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
|
||||
@@ -85,7 +85,7 @@ impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> AnyProp
|
||||
{
|
||||
tracing::error!("Panic while rendering component `{_name}`: {err:?}");
|
||||
}
|
||||
Element::Err(CapturedPanic { error: err }.into())
|
||||
Element::Err(CapturedPanic(err).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::{
|
||||
ElementRef, MountId, ScopeOrder, SuspenseBoundaryProps, SuspenseBoundaryPropsWithOwner,
|
||||
VComponent, WriteMutations,
|
||||
},
|
||||
nodes::{AsVNode, VNode},
|
||||
scopes::ScopeId,
|
||||
nodes::VNode,
|
||||
scopes::{LastRenderedNode, ScopeId},
|
||||
virtual_dom::VirtualDom,
|
||||
Element, SuspenseContext,
|
||||
};
|
||||
@@ -50,10 +50,9 @@ impl VirtualDom {
|
||||
// If it is suspended, we need to diff it but write the mutations nothing
|
||||
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
|
||||
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
|
||||
old.as_vnode()
|
||||
.diff_node(new_real_nodes, self, render_to.as_deref_mut());
|
||||
old.diff_node(new_real_nodes, self, render_to.as_deref_mut());
|
||||
|
||||
self.scopes[scope.0].last_rendered_node = Some(new_nodes);
|
||||
self.scopes[scope.0].last_rendered_node = Some(LastRenderedNode::new(new_nodes));
|
||||
|
||||
if render_to.is_some() {
|
||||
self.runtime.get_state(scope).unwrap().mount(&self.runtime);
|
||||
@@ -69,7 +68,7 @@ impl VirtualDom {
|
||||
&mut self,
|
||||
to: Option<&mut M>,
|
||||
scope: ScopeId,
|
||||
new_nodes: Element,
|
||||
new_nodes: LastRenderedNode,
|
||||
parent: Option<ElementRef>,
|
||||
) -> usize {
|
||||
self.runtime.clone().with_scope_on_stack(scope, || {
|
||||
@@ -79,9 +78,7 @@ impl VirtualDom {
|
||||
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
|
||||
|
||||
// Create the node
|
||||
let nodes = new_nodes
|
||||
.as_vnode()
|
||||
.create(self, parent, render_to.as_deref_mut());
|
||||
let nodes = new_nodes.create(self, parent, render_to.as_deref_mut());
|
||||
|
||||
// Then set the new node as the last rendered node
|
||||
self.scopes[scope.0].last_rendered_node = Some(new_nodes);
|
||||
@@ -105,13 +102,8 @@ impl VirtualDom {
|
||||
SuspenseContext::remove_suspended_nodes::<M>(self, scope_id, destroy_component_state);
|
||||
|
||||
// Remove the component from the dom
|
||||
if let Some(node) = self.scopes[scope_id.0].last_rendered_node.as_ref() {
|
||||
node.clone().as_vnode().remove_node_inner(
|
||||
self,
|
||||
to,
|
||||
destroy_component_state,
|
||||
replace_with,
|
||||
)
|
||||
if let Some(node) = self.scopes[scope_id.0].last_rendered_node.clone() {
|
||||
node.remove_node_inner(self, to, destroy_component_state, replace_with)
|
||||
};
|
||||
|
||||
if destroy_component_state {
|
||||
@@ -210,16 +202,15 @@ impl VNode {
|
||||
let new = dom.run_scope(scope_id);
|
||||
|
||||
// Then set the new node as the last rendered node
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new);
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(LastRenderedNode::new(new));
|
||||
}
|
||||
|
||||
let scope = ScopeId(dom.get_mounted_dyn_node(mount, idx));
|
||||
|
||||
let new_node = dom.scopes[scope.0]
|
||||
.last_rendered_node
|
||||
.as_ref()
|
||||
.expect("Component to be mounted")
|
||||
.clone();
|
||||
.clone()
|
||||
.expect("Component to be mounted");
|
||||
|
||||
dom.create_scope(to, scope, new_node, parent)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
use crate::{
|
||||
global_context::current_scope_id, innerlude::provide_context, try_consume_context, use_hook,
|
||||
Element, IntoDynNode, Properties, ReactiveContext, ScopeId, Subscribers, Template,
|
||||
TemplateAttribute, TemplateNode, VNode,
|
||||
innerlude::{provide_context, CapturedError},
|
||||
try_consume_context, use_hook, Element, IntoDynNode, Properties, ReactiveContext, Subscribers,
|
||||
Template, TemplateAttribute, TemplateNode, VNode,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
backtrace::Backtrace,
|
||||
cell::{Ref, RefCell},
|
||||
error::Error,
|
||||
cell::RefCell,
|
||||
fmt::{Debug, Display},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Return early with an error.
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($msg:literal $(,)?) => {
|
||||
return $crate::internal::Err($crate::internal::__anyhow!($msg).into())
|
||||
};
|
||||
($err:expr $(,)?) => {
|
||||
return $crate::internal::Err($crate::internal::__anyhow!($err).into())
|
||||
};
|
||||
($fmt:expr, $($arg:tt)*) => {
|
||||
return $crate::internal::Err($crate::internal::__anyhow!($fmt, $($arg)*).into())
|
||||
};
|
||||
}
|
||||
|
||||
/// A panic in a component that was caught by an error boundary.
|
||||
///
|
||||
/// <div class="warning">
|
||||
@@ -20,12 +31,9 @@ use std::{
|
||||
/// WASM currently does not support caching unwinds, so this struct will not be created in WASM.
|
||||
///
|
||||
/// </div>
|
||||
pub struct CapturedPanic {
|
||||
#[allow(dead_code)]
|
||||
/// The error that was caught
|
||||
pub error: Box<dyn Any + 'static>,
|
||||
}
|
||||
|
||||
pub(crate) struct CapturedPanic(pub(crate) Box<dyn Any + Send + 'static>);
|
||||
unsafe impl Sync for CapturedPanic {}
|
||||
impl std::error::Error for CapturedPanic {}
|
||||
impl Debug for CapturedPanic {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CapturedPanic").finish()
|
||||
@@ -34,12 +42,10 @@ impl Debug for CapturedPanic {
|
||||
|
||||
impl Display for CapturedPanic {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("Encountered panic: {:?}", self.error))
|
||||
f.write_fmt(format_args!("Encountered panic: {:?}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CapturedPanic {}
|
||||
|
||||
/// A context supplied by fullstack to create hydration compatible error boundaries. Generally, this
|
||||
/// is not present and the default in memory error boundary is used. If fullstack is enabled, it will
|
||||
/// provide its own factory that handles syncing errors to the hydration context
|
||||
@@ -48,7 +54,7 @@ struct CreateErrorBoundary(fn() -> ErrorContext);
|
||||
|
||||
impl Default for CreateErrorBoundary {
|
||||
fn default() -> Self {
|
||||
Self(|| ErrorContext::new(Vec::new()))
|
||||
Self(|| ErrorContext::new(None))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,241 +77,57 @@ pub fn use_error_boundary_provider() -> ErrorContext {
|
||||
use_hook(|| provide_context(create_error_boundary()))
|
||||
}
|
||||
|
||||
/// A trait for any type that can be downcast to a concrete type and implements Debug. This is automatically implemented for all types that implement Any + Debug.
|
||||
pub trait AnyError {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_error(&self) -> &dyn Error;
|
||||
}
|
||||
|
||||
/// An wrapper error type for types that only implement Display. We use a inner type here to avoid overlapping implementations for DisplayError and impl Error
|
||||
struct DisplayError(DisplayErrorInner);
|
||||
|
||||
impl<E: Display + 'static> From<E> for DisplayError {
|
||||
fn from(e: E) -> Self {
|
||||
Self(DisplayErrorInner(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayErrorInner(Box<dyn Display>);
|
||||
impl Display for DisplayErrorInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for DisplayErrorInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DisplayErrorInner {}
|
||||
|
||||
impl AnyError for DisplayError {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
&self.0 .0
|
||||
}
|
||||
|
||||
fn as_error(&self) -> &dyn Error {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides context methods to [`Result`] and [`Option`] types that are compatible with [`CapturedError`]
|
||||
///
|
||||
/// This trait is sealed and cannot be implemented outside of dioxus-core
|
||||
pub trait Context<T, E>: private::Sealed {
|
||||
/// Add a visual representation of the error that the [`ErrorBoundary`] may render
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn Component() -> Element {
|
||||
/// // You can bubble up errors with `?` inside components, and event handlers
|
||||
/// // Along with the error itself, you can provide a way to display the error by calling `show`
|
||||
/// let number = "1234".parse::<usize>().show(|error| rsx! {
|
||||
/// div {
|
||||
/// background_color: "red",
|
||||
/// color: "white",
|
||||
/// "Error parsing number: {error}"
|
||||
/// }
|
||||
/// })?;
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// ```
|
||||
fn show(self, display_error: impl FnOnce(&E) -> Element) -> Result<T>;
|
||||
|
||||
/// Wrap the result additional context about the error that occurred.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn NumberParser() -> Element {
|
||||
/// // You can bubble up errors with `?` inside components, and event handlers
|
||||
/// // Along with the error itself, you can provide a way to display the error by calling `context`
|
||||
/// let number = "-1234".parse::<usize>().context("Parsing number inside of the NumberParser")?;
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// ```
|
||||
fn context<C: Display + 'static>(self, context: C) -> Result<T>;
|
||||
|
||||
/// Wrap the result with additional context about the error that occurred. The closure will only be run if the Result is an error.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// fn NumberParser() -> Element {
|
||||
/// // You can bubble up errors with `?` inside components, and event handlers
|
||||
/// // Along with the error itself, you can provide a way to display the error by calling `context`
|
||||
/// let number = "-1234".parse::<usize>().with_context(|| format!("Timestamp: {:?}", std::time::Instant::now()))?;
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// ```
|
||||
fn with_context<C: Display + 'static>(self, context: impl FnOnce() -> C) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T, E> Context<T, E> for std::result::Result<T, E>
|
||||
where
|
||||
E: Error + 'static,
|
||||
{
|
||||
fn show(self, display_error: impl FnOnce(&E) -> Element) -> Result<T> {
|
||||
// We don't use result mapping to avoid extra frames
|
||||
match self {
|
||||
std::result::Result::Ok(value) => Ok(value),
|
||||
Err(error) => {
|
||||
let render = display_error(&error).unwrap_or_default();
|
||||
let mut error: CapturedError = error.into();
|
||||
error.render = render;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn context<C: Display + 'static>(self, context: C) -> Result<T> {
|
||||
self.with_context(|| context)
|
||||
}
|
||||
|
||||
fn with_context<C: Display + 'static>(self, context: impl FnOnce() -> C) -> Result<T> {
|
||||
// We don't use result mapping to avoid extra frames
|
||||
match self {
|
||||
std::result::Result::Ok(value) => Ok(value),
|
||||
Err(error) => {
|
||||
let mut error: CapturedError = error.into();
|
||||
error.context.push(Rc::new(AdditionalErrorContext {
|
||||
backtrace: Backtrace::capture(),
|
||||
context: Box::new(context()),
|
||||
scope: current_scope_id().ok(),
|
||||
}));
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Context<T, CapturedError> for Option<T> {
|
||||
fn show(self, display_error: impl FnOnce(&CapturedError) -> Element) -> Result<T> {
|
||||
// We don't use result mapping to avoid extra frames
|
||||
match self {
|
||||
Some(value) => Ok(value),
|
||||
None => {
|
||||
let mut error = CapturedError::from_display("Value was none");
|
||||
let render = display_error(&error).unwrap_or_default();
|
||||
error.render = render;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn context<C: Display + 'static>(self, context: C) -> Result<T> {
|
||||
self.with_context(|| context)
|
||||
}
|
||||
|
||||
fn with_context<C: Display + 'static>(self, context: impl FnOnce() -> C) -> Result<T> {
|
||||
// We don't use result mapping to avoid extra frames
|
||||
match self {
|
||||
Some(value) => Ok(value),
|
||||
None => {
|
||||
let error = CapturedError::from_display(context());
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl<T, E> Sealed for std::result::Result<T, E> where E: Error {}
|
||||
impl<T> Sealed for Option<T> {}
|
||||
}
|
||||
|
||||
impl<T: Any + Error> AnyError for T {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_error(&self) -> &dyn Error {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A context with information about suspended components
|
||||
#[derive(Clone)]
|
||||
pub struct ErrorContext {
|
||||
errors: Rc<RefCell<Vec<CapturedError>>>,
|
||||
error: Rc<RefCell<Option<CapturedError>>>,
|
||||
subscribers: Subscribers,
|
||||
}
|
||||
|
||||
impl Debug for ErrorContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ErrorContext")
|
||||
.field("errors", &self.errors)
|
||||
.field("error", &self.error)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ErrorContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.errors, &other.errors)
|
||||
Rc::ptr_eq(&self.error, &other.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorContext {
|
||||
/// Create a new in memory error context. This will not be serialized to the hydration context if fullstack
|
||||
/// is enabled
|
||||
pub fn new(errors: Vec<CapturedError>) -> Self {
|
||||
/// Create a new suspense boundary in a specific scope
|
||||
pub fn new(error: Option<CapturedError>) -> Self {
|
||||
Self {
|
||||
errors: Rc::new(RefCell::new(errors)),
|
||||
subscribers: Subscribers::default(),
|
||||
error: Rc::new(RefCell::new(error)),
|
||||
subscribers: Subscribers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all errors thrown from child components
|
||||
pub fn errors(&self) -> Ref<'_, [CapturedError]> {
|
||||
/// Get the current error, if any. If multiple components have errored, this will return the first
|
||||
/// error that made it to this boundary.
|
||||
pub fn error(&self) -> Option<CapturedError> {
|
||||
// Subscribe to the current reactive context if one exists. This is usually
|
||||
// the error boundary component that is rendering the errors
|
||||
if let Some(rc) = ReactiveContext::current() {
|
||||
self.subscribers.add(rc);
|
||||
}
|
||||
Ref::map(self.errors.borrow(), |errors| errors.as_slice())
|
||||
}
|
||||
|
||||
/// Get the Element from the first error that can be shown
|
||||
pub fn show(&self) -> Option<Element> {
|
||||
self.errors.borrow().iter().find_map(|task| task.show())
|
||||
self.error.borrow().clone()
|
||||
}
|
||||
|
||||
/// Push an error into this Error Boundary
|
||||
pub fn insert_error(&self, error: CapturedError) {
|
||||
self.errors.borrow_mut().push(error);
|
||||
self.error.borrow_mut().replace(error);
|
||||
self.mark_dirty()
|
||||
}
|
||||
|
||||
/// Clear all errors from this Error Boundary
|
||||
pub fn clear_errors(&self) {
|
||||
self.errors.borrow_mut().clear();
|
||||
self.error.borrow_mut().take();
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
@@ -321,244 +143,6 @@ impl ErrorContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors can have additional context added as they bubble up the render tree
|
||||
/// This context can be used to provide additional information to the user
|
||||
struct AdditionalErrorContext {
|
||||
backtrace: Backtrace,
|
||||
context: Box<dyn Display>,
|
||||
scope: Option<ScopeId>,
|
||||
}
|
||||
|
||||
impl Debug for AdditionalErrorContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ErrorContext")
|
||||
.field("backtrace", &self.backtrace)
|
||||
.field("context", &self.context.to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AdditionalErrorContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let AdditionalErrorContext {
|
||||
backtrace,
|
||||
context,
|
||||
scope,
|
||||
} = self;
|
||||
|
||||
write!(f, "{context} (from ")?;
|
||||
|
||||
if let Some(scope) = scope {
|
||||
write!(f, "scope {scope:?} ")?;
|
||||
}
|
||||
|
||||
write!(f, "at {backtrace:?})")
|
||||
}
|
||||
}
|
||||
|
||||
/// A type alias for a result that can be either a boxed error or a value
|
||||
/// This is useful to avoid having to use `Result<T, CapturedError>` everywhere
|
||||
pub type Result<T = ()> = std::result::Result<T, CapturedError>;
|
||||
|
||||
/// A helper function for an Ok result that can be either a boxed error or a value
|
||||
/// This is useful to avoid having to use `Ok<T, CapturedError>` everywhere
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Ok<T>(value: T) -> Result<T> {
|
||||
Result::Ok(value)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// An instance of an error captured by a descendant component.
|
||||
pub struct CapturedError {
|
||||
/// The error captured by the error boundary
|
||||
error: Rc<dyn AnyError + 'static>,
|
||||
|
||||
/// The backtrace of the error
|
||||
backtrace: Rc<Backtrace>,
|
||||
|
||||
/// The scope that threw the error
|
||||
scope: ScopeId,
|
||||
|
||||
/// An error message that can be displayed to the user
|
||||
pub(crate) render: VNode,
|
||||
|
||||
/// Additional context that was added to the error
|
||||
context: Vec<Rc<AdditionalErrorContext>>,
|
||||
}
|
||||
|
||||
impl FromStr for CapturedError {
|
||||
type Err = std::convert::Infallible;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
std::result::Result::Ok(Self::from_display(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct SerializedCapturedError {
|
||||
error: String,
|
||||
context: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl serde::Serialize for CapturedError {
|
||||
fn serialize<S: serde::Serializer>(
|
||||
&self,
|
||||
serializer: S,
|
||||
) -> std::result::Result<S::Ok, S::Error> {
|
||||
let serialized = SerializedCapturedError {
|
||||
error: self.error.as_error().to_string(),
|
||||
context: self
|
||||
.context
|
||||
.iter()
|
||||
.map(|context| context.to_string())
|
||||
.collect(),
|
||||
};
|
||||
serialized.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl<'de> serde::Deserialize<'de> for CapturedError {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<Self, D::Error> {
|
||||
let serialized = SerializedCapturedError::deserialize(deserializer)?;
|
||||
|
||||
let error = DisplayError::from(serialized.error);
|
||||
let context = serialized
|
||||
.context
|
||||
.into_iter()
|
||||
.map(|context| {
|
||||
Rc::new(AdditionalErrorContext {
|
||||
scope: None,
|
||||
backtrace: Backtrace::disabled(),
|
||||
context: Box::new(context),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
std::result::Result::Ok(Self {
|
||||
error: Rc::new(error),
|
||||
context,
|
||||
backtrace: Rc::new(Backtrace::disabled()),
|
||||
scope: ScopeId::ROOT,
|
||||
render: VNode::placeholder(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for CapturedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CapturedError")
|
||||
.field("error", &self.error.as_error())
|
||||
.field("backtrace", &self.backtrace)
|
||||
.field("scope", &self.scope)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: AnyError + 'static> From<E> for CapturedError {
|
||||
fn from(error: E) -> Self {
|
||||
Self {
|
||||
error: Rc::new(error),
|
||||
backtrace: Rc::new(Backtrace::capture()),
|
||||
scope: current_scope_id()
|
||||
.expect("Cannot create an error boundary outside of a component's scope."),
|
||||
render: Default::default(),
|
||||
context: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CapturedError {
|
||||
/// Create a new captured error
|
||||
pub fn new(error: impl AnyError + 'static) -> Self {
|
||||
Self {
|
||||
error: Rc::new(error),
|
||||
backtrace: Rc::new(Backtrace::capture()),
|
||||
scope: current_scope_id().unwrap_or(ScopeId::ROOT),
|
||||
render: Default::default(),
|
||||
context: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new error from a type that only implements [`Display`]. If your type implements [`Error`], you can use [`CapturedError::from`] instead.
|
||||
pub fn from_display(error: impl Display + 'static) -> Self {
|
||||
Self {
|
||||
error: Rc::new(DisplayError::from(error)),
|
||||
backtrace: Rc::new(Backtrace::capture()),
|
||||
scope: current_scope_id().unwrap_or(ScopeId::ROOT),
|
||||
render: Default::default(),
|
||||
context: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the error as being thrown from a specific scope
|
||||
pub fn with_origin(mut self, scope: ScopeId) -> Self {
|
||||
self.scope = scope;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get a VNode representation of the error if the error provides one
|
||||
pub fn show(&self) -> Option<Element> {
|
||||
if self.render == VNode::placeholder() {
|
||||
None
|
||||
} else {
|
||||
Some(std::result::Result::Ok(self.render.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a deep clone of this error
|
||||
pub(crate) fn deep_clone(&self) -> Self {
|
||||
Self {
|
||||
render: self.render.deep_clone(),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CapturedError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
format!("{:?}", self) == format!("{:?}", other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CapturedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}\nContext: ",
|
||||
self.error.as_error(),
|
||||
self.scope,
|
||||
self.backtrace
|
||||
))?;
|
||||
for context in &*self.context {
|
||||
f.write_fmt(format_args!("{}\n", context))?;
|
||||
}
|
||||
std::result::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CapturedError {
|
||||
/// Downcast the error type into a concrete error type
|
||||
pub fn downcast<T: 'static>(&self) -> Option<&T> {
|
||||
self.error.as_any().downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn throw_into(error: impl Into<CapturedError>, scope: ScopeId) {
|
||||
let error = error.into();
|
||||
if let Some(cx) = scope.consume_context::<ErrorContext>() {
|
||||
cx.insert_error(error)
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Tried to throw an error into an error boundary, but failed to locate a boundary: {:?}",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Clone)]
|
||||
pub struct ErrorHandler(Rc<dyn Fn(ErrorContext) -> Element>);
|
||||
@@ -587,7 +171,7 @@ fn default_handler(errors: ErrorContext) -> Element {
|
||||
None,
|
||||
TEMPLATE,
|
||||
Box::new([errors
|
||||
.errors()
|
||||
.error()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
static TEMPLATE: Template = Template {
|
||||
@@ -628,6 +212,7 @@ impl ErrorBoundaryProps {
|
||||
ErrorBoundaryPropsBuilder { fields: ((), ()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[doc(hidden)]
|
||||
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||
@@ -850,8 +435,9 @@ impl<
|
||||
#[allow(non_upper_case_globals, non_snake_case)]
|
||||
pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
|
||||
let error_boundary = use_error_boundary_provider();
|
||||
let errors = error_boundary.errors();
|
||||
let has_errors = !errors.is_empty();
|
||||
let errors = error_boundary.error();
|
||||
let has_errors = errors.is_some();
|
||||
|
||||
// Drop errors before running user code that might borrow the error lock
|
||||
drop(errors);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{current_scope_id, properties::SuperFrom, runtime::RuntimeGuard, Runtime, ScopeId};
|
||||
use futures_util::FutureExt;
|
||||
use generational_box::GenerationalBox;
|
||||
use std::{any::Any, cell::RefCell, marker::PhantomData, panic::Location, rc::Rc};
|
||||
|
||||
@@ -335,9 +336,15 @@ impl<Ret> SpawnIfAsync<(), Ret> for Ret {
|
||||
pub struct AsyncMarker;
|
||||
impl<F: std::future::Future<Output = ()> + 'static> SpawnIfAsync<AsyncMarker> for F {
|
||||
fn spawn(self) {
|
||||
crate::spawn(async move {
|
||||
self.await;
|
||||
});
|
||||
// Quick poll once to deal with things like prevent_default in the same tick
|
||||
let mut fut = Box::pin(self);
|
||||
let res = fut.as_mut().now_or_never();
|
||||
|
||||
if res.is_none() {
|
||||
crate::spawn(async move {
|
||||
fut.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,11 +358,17 @@ where
|
||||
{
|
||||
#[inline]
|
||||
fn spawn(self) {
|
||||
crate::spawn(async move {
|
||||
if let Err(err) = self.await {
|
||||
crate::throw_error(err)
|
||||
}
|
||||
});
|
||||
// Quick poll once to deal with things like prevent_default in the same tick
|
||||
let mut fut = Box::pin(self);
|
||||
let res = fut.as_mut().now_or_never();
|
||||
|
||||
if res.is_none() {
|
||||
crate::spawn(async move {
|
||||
if let Err(err) = fut.await {
|
||||
crate::throw_error(err)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::runtime::RuntimeError;
|
||||
use crate::{innerlude::CapturedError, runtime::RuntimeError};
|
||||
use crate::{
|
||||
innerlude::SuspendedFuture, runtime::Runtime, CapturedError, Element, ScopeId, SuspenseContext,
|
||||
Task,
|
||||
innerlude::SuspendedFuture, runtime::Runtime, Element, ScopeId, SuspenseContext, Task,
|
||||
};
|
||||
use std::future::Future;
|
||||
use std::rc::Rc;
|
||||
|
||||
+27
-13
@@ -41,6 +41,14 @@ pub mod internal {
|
||||
TemplateGlobalKey,
|
||||
};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[doc(hidden)]
|
||||
pub fn Err<T, E>(e: E) -> Result<T, E> {
|
||||
std::result::Result::Err(e)
|
||||
}
|
||||
|
||||
pub use anyhow::__anyhow;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use generational_box;
|
||||
}
|
||||
@@ -67,6 +75,11 @@ pub(crate) mod innerlude {
|
||||
pub use crate::tasks::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
pub use anyhow::anyhow;
|
||||
pub use anyhow::Context as AnyhowContext;
|
||||
pub use anyhow::Error;
|
||||
pub use anyhow::Result;
|
||||
|
||||
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`ScopeId`] or [`ScopeState`].
|
||||
///
|
||||
/// An Errored [`Element`] will propagate the error to the nearest error boundary.
|
||||
@@ -77,21 +90,22 @@ pub(crate) mod innerlude {
|
||||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
consume_context, consume_context_from_scope, current_owner, current_scope_id, fc_to_builder,
|
||||
force_all_dirty, generation, has_context, needs_update, needs_update_any, parent_scope,
|
||||
provide_context, provide_create_error_boundary, provide_root_context, queue_effect,
|
||||
remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, spawn_isomorphic,
|
||||
suspend, suspense_context, throw_error, try_consume_context, use_after_render,
|
||||
use_before_render, use_drop, use_error_boundary_provider, use_hook, use_hook_with_cleanup,
|
||||
vdom_is_rendering, with_owner, AnyValue, Attribute, AttributeValue, Callback, CapturedError,
|
||||
Component, ComponentFunction, Context, DynamicNode, Element, ElementId, ErrorBoundary,
|
||||
ErrorContext, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode,
|
||||
LaunchConfig, ListenerCallback, MarkerWrapper, Mutation, Mutations, NoOpMutations, Ok,
|
||||
anyhow, consume_context, consume_context_from_scope, current_owner, current_scope_id,
|
||||
fc_to_builder, force_all_dirty, generation, has_context, needs_update, needs_update_any,
|
||||
parent_scope, provide_context, provide_create_error_boundary, provide_root_context,
|
||||
queue_effect, remove_future, schedule_update, schedule_update_any, spawn, spawn_forever,
|
||||
spawn_isomorphic, suspend, suspense_context, throw_error, try_consume_context,
|
||||
use_after_render, use_before_render, use_drop, use_hook, use_hook_with_cleanup,
|
||||
vdom_is_rendering, with_owner, AnyValue, AnyhowContext, Attribute, AttributeValue, Callback,
|
||||
CapturedError, Component, ComponentFunction, DynamicNode, Element, ElementId, Error,
|
||||
ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
|
||||
IntoDynNode, LaunchConfig, ListenerCallback, MarkerWrapper, Mutation, Mutations, NoOpMutations,
|
||||
OptionStringFromMarker, Properties, ReactiveContext, RenderError, Result, Runtime,
|
||||
RuntimeGuard, ScopeId, ScopeState, SpawnIfAsync, SubscriberList, Subscribers, SuperFrom,
|
||||
SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuspenseContext,
|
||||
SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode,
|
||||
VNodeInner, VPlaceholder, VText, VirtualDom, WriteMutations,
|
||||
SuperInto, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuspenseContext, Task,
|
||||
Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, VText,
|
||||
VirtualDom, WriteMutations,
|
||||
};
|
||||
|
||||
pub use anyhow::Ok;
|
||||
pub use const_format;
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
events::ListenerCallback,
|
||||
innerlude::{ElementRef, MountId, ScopeState, VProps},
|
||||
properties::ComponentFunction,
|
||||
Element, Event, Properties, RenderError, ScopeId, VirtualDom,
|
||||
Element, Event, Properties, ScopeId, VirtualDom,
|
||||
};
|
||||
use dioxus_core_types::DioxusFormattable;
|
||||
use std::ops::Deref;
|
||||
@@ -100,55 +100,6 @@ pub struct VNode {
|
||||
pub(crate) mount: Cell<MountId>,
|
||||
}
|
||||
|
||||
impl AsRef<VNode> for Element {
|
||||
fn as_ref(&self) -> &VNode {
|
||||
match self {
|
||||
Element::Ok(node) => node,
|
||||
Element::Err(RenderError::Aborted(err)) => &err.render,
|
||||
Element::Err(RenderError::Suspended(fut)) => &fut.placeholder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Element> for VNode {
|
||||
fn from(val: &Element) -> Self {
|
||||
AsRef::as_ref(val).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Element> for VNode {
|
||||
fn from(val: Element) -> Self {
|
||||
match val {
|
||||
Element::Ok(node) => node,
|
||||
Element::Err(RenderError::Aborted(err)) => err.render,
|
||||
Element::Err(RenderError::Suspended(fut)) => fut.placeholder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A tiny helper trait to get the vnode for a Element
|
||||
pub(crate) trait AsVNode {
|
||||
/// Get the vnode for this element
|
||||
fn as_vnode(&self) -> &VNode;
|
||||
|
||||
/// Create a deep clone of this VNode
|
||||
fn deep_clone(&self) -> Self;
|
||||
}
|
||||
|
||||
impl AsVNode for Element {
|
||||
fn as_vnode(&self) -> &VNode {
|
||||
AsRef::as_ref(self)
|
||||
}
|
||||
|
||||
fn deep_clone(&self) -> Self {
|
||||
match self {
|
||||
Ok(node) => Ok(node.deep_clone()),
|
||||
Err(RenderError::Aborted(err)) => Err(RenderError::Aborted(err.deep_clone())),
|
||||
Err(RenderError::Suspended(fut)) => Err(RenderError::Suspended(fut.deep_clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VNode {
|
||||
fn default() -> Self {
|
||||
Self::placeholder()
|
||||
@@ -979,7 +930,7 @@ impl IntoVNode for Element {
|
||||
fn into_vnode(self) -> VNode {
|
||||
match self {
|
||||
Ok(val) => val.into_vnode(),
|
||||
_ => VNode::empty().unwrap(),
|
||||
_ => VNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -987,7 +938,7 @@ impl IntoVNode for &Element {
|
||||
fn into_vnode(self) -> VNode {
|
||||
match self {
|
||||
Ok(val) => val.into_vnode(),
|
||||
_ => VNode::empty().unwrap(),
|
||||
_ => VNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -995,7 +946,7 @@ impl IntoVNode for Option<VNode> {
|
||||
fn into_vnode(self) -> VNode {
|
||||
match self {
|
||||
Some(val) => val.into_vnode(),
|
||||
_ => VNode::empty().unwrap(),
|
||||
_ => VNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1003,7 +954,7 @@ impl IntoVNode for &Option<VNode> {
|
||||
fn into_vnode(self) -> VNode {
|
||||
match self.as_ref() {
|
||||
Some(val) => val.clone().into_vnode(),
|
||||
_ => VNode::empty().unwrap(),
|
||||
_ => VNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,7 +962,7 @@ impl IntoVNode for Option<Element> {
|
||||
fn into_vnode(self) -> VNode {
|
||||
match self {
|
||||
Some(val) => val.into_vnode(),
|
||||
_ => VNode::empty().unwrap(),
|
||||
_ => VNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1019,7 +970,7 @@ impl IntoVNode for &Option<Element> {
|
||||
fn into_vnode(self) -> VNode {
|
||||
match self.as_ref() {
|
||||
Some(val) => val.clone().into_vnode(),
|
||||
_ => VNode::empty().unwrap(),
|
||||
_ => VNode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ impl ReactiveContext {
|
||||
let id = scope.id;
|
||||
let sender = runtime.sender.clone();
|
||||
let update_scope = move || {
|
||||
tracing::trace!("Marking scope {:?} as dirty", id);
|
||||
sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
_ = sender.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
};
|
||||
|
||||
// Otherwise, create a new context at the current scope
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::innerlude::*;
|
||||
|
||||
/// An error that can occur while rendering a component
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RenderError {
|
||||
/// The render function returned early
|
||||
Aborted(CapturedError),
|
||||
/// The render function returned early due to an error.
|
||||
///
|
||||
/// We captured the error, wrapped it in an Arc, and stored it here. You can no longer modify the error,
|
||||
/// but you can cheaply pass it around.
|
||||
Error(CapturedError),
|
||||
|
||||
/// The component was suspended
|
||||
Suspended(SuspendedFuture),
|
||||
@@ -26,27 +32,103 @@ impl Default for RenderError {
|
||||
}
|
||||
}
|
||||
impl std::error::Error for RenderAbortedEarly {}
|
||||
Self::Aborted(RenderAbortedEarly.into())
|
||||
Self::Error(CapturedError(Arc::new(RenderAbortedEarly.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RenderError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Aborted(e) => write!(f, "Render aborted: {e}"),
|
||||
Self::Error(e) => write!(f, "Render aborted: {e}"),
|
||||
Self::Suspended(e) => write!(f, "Component suspended: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: std::error::Error + 'static> From<E> for RenderError {
|
||||
fn from(e: E) -> Self {
|
||||
Self::Aborted(CapturedError::from(e))
|
||||
impl From<CapturedError> for RenderError {
|
||||
fn from(e: CapturedError) -> Self {
|
||||
Self::Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CapturedError> for RenderError {
|
||||
fn from(e: CapturedError) -> Self {
|
||||
RenderError::Aborted(e)
|
||||
impl<E: Into<Error>> From<E> for RenderError {
|
||||
fn from(e: E) -> Self {
|
||||
let anyhow_err = e.into();
|
||||
|
||||
if let Some(suspended) = anyhow_err.downcast_ref::<SuspendedFuture>() {
|
||||
return Self::Suspended(suspended.clone());
|
||||
}
|
||||
|
||||
if let Some(render_error) = anyhow_err.downcast_ref::<RenderError>() {
|
||||
return render_error.clone();
|
||||
}
|
||||
|
||||
Self::Error(CapturedError(Arc::new(anyhow_err)))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `anyhow::Error` wrapped in an `Arc` so it can be cheaply cloned and passed around.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CapturedError(pub Arc<Error>);
|
||||
|
||||
impl CapturedError {
|
||||
/// Create a `CapturedError` from anything that implements `Display`.
|
||||
pub fn from_display(t: impl Display) -> Self {
|
||||
Self(Arc::new(anyhow::anyhow!(t.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl serde::Serialize for CapturedError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl<'de> serde::Deserialize<'de> for CapturedError {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Ok(Self::from_display(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for CapturedError {
|
||||
type Target = Error;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<Error>> From<E> for CapturedError {
|
||||
fn from(e: E) -> Self {
|
||||
Self(Arc::new(e.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CapturedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CapturedError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_errs_can_downcast() {
|
||||
fn assert_is_stderr_like<T: Send + Sync + Display + Debug>() {}
|
||||
|
||||
assert_is_stderr_like::<RenderError>();
|
||||
assert_is_stderr_like::<CapturedError>();
|
||||
}
|
||||
@@ -250,7 +250,7 @@ impl Runtime {
|
||||
|
||||
/// Pops a scope off the stack
|
||||
pub(crate) fn pop() {
|
||||
RUNTIMES.with(|stack| stack.borrow_mut().pop());
|
||||
RUNTIMES.with(|stack| stack.borrow_mut().pop().unwrap());
|
||||
}
|
||||
|
||||
/// Runs a function with the current runtime
|
||||
@@ -273,11 +273,10 @@ impl Runtime {
|
||||
/// Runs a function with the current scope
|
||||
pub(crate) fn with_scope<R>(
|
||||
scope: ScopeId,
|
||||
f: impl FnOnce(&Scope) -> R,
|
||||
callback: impl FnOnce(&Scope) -> R,
|
||||
) -> Result<R, RuntimeError> {
|
||||
Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc)))
|
||||
.ok()
|
||||
.flatten()
|
||||
Self::with(|rt| rt.get_state(scope).map(|scopestate| callback(&scopestate)))
|
||||
.expect("Runtime should exist")
|
||||
.ok_or(RuntimeError::new())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use crate::{
|
||||
any_props::{AnyProps, BoxedAnyProps},
|
||||
innerlude::{throw_error, RenderError, ScopeOrder, ScopeState},
|
||||
nodes::AsVNode,
|
||||
scope_context::{Scope, SuspenseLocation},
|
||||
scopes::ScopeId,
|
||||
virtual_dom::VirtualDom,
|
||||
ReactiveContext,
|
||||
Element, ReactiveContext,
|
||||
};
|
||||
use crate::{Element, VNode};
|
||||
|
||||
impl VirtualDom {
|
||||
pub(super) fn new_scope(
|
||||
@@ -39,7 +37,6 @@ impl VirtualDom {
|
||||
});
|
||||
|
||||
self.runtime.create_scope(scope_runtime);
|
||||
tracing::trace!("created scope {id:?} with parent {parent_id:?}");
|
||||
|
||||
scope
|
||||
}
|
||||
@@ -79,7 +76,14 @@ impl VirtualDom {
|
||||
// the component runs, it returns a clone of the last rsx that was returned from
|
||||
// that resource. If we don't deep clone the VNode and the resource changes, then
|
||||
// we could end up diffing two different versions of the same mounted node
|
||||
let mut render_return = render_return.deep_clone();
|
||||
let mut render_return = match render_return {
|
||||
Ok(node) => Ok(node.deep_clone()),
|
||||
Err(RenderError::Error(err)) => Err(RenderError::Error(err.clone())),
|
||||
Err(RenderError::Suspended(fut)) => {
|
||||
Err(RenderError::Suspended(fut.deep_clone()))
|
||||
}
|
||||
};
|
||||
|
||||
self.handle_element_return(&mut render_return, scope_id, &scope.state());
|
||||
render_return
|
||||
})
|
||||
@@ -103,13 +107,12 @@ impl VirtualDom {
|
||||
/// Insert any errors, or suspended tasks from an element return into the runtime
|
||||
fn handle_element_return(&self, node: &mut Element, scope_id: ScopeId, scope_state: &Scope) {
|
||||
match node {
|
||||
Err(RenderError::Aborted(e)) => {
|
||||
Err(RenderError::Error(e)) => {
|
||||
tracing::error!(
|
||||
"Error while rendering component `{}`:\n{e}",
|
||||
scope_state.name
|
||||
);
|
||||
throw_error(e.clone());
|
||||
e.render = VNode::placeholder();
|
||||
}
|
||||
Err(RenderError::Suspended(e)) => {
|
||||
let task = e.task();
|
||||
@@ -136,7 +139,6 @@ impl VirtualDom {
|
||||
.suspended_tasks
|
||||
.set(self.runtime.suspended_tasks.get() + 1);
|
||||
}
|
||||
e.placeholder = VNode::placeholder();
|
||||
}
|
||||
Ok(_) => {
|
||||
// If the render was successful, we can move the render generation forward by one
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
innerlude::{throw_into, CapturedError, SchedulerMsg, SuspenseContext},
|
||||
innerlude::{CapturedError, SchedulerMsg, SuspenseContext},
|
||||
runtime::RuntimeError,
|
||||
Runtime, ScopeId, Task,
|
||||
};
|
||||
@@ -167,7 +167,7 @@ impl Scope {
|
||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
let chan = self.sender();
|
||||
Arc::new(move |id| {
|
||||
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
_ = chan.unbounded_send(SchedulerMsg::Immediate(id));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -545,9 +545,7 @@ impl ScopeId {
|
||||
|
||||
/// Consume context from the current scope
|
||||
pub fn consume_context<T: 'static + Clone>(self) -> Option<T> {
|
||||
Runtime::with_scope(self, |cx| cx.consume_context::<T>())
|
||||
.ok()
|
||||
.flatten()
|
||||
Runtime::with_scope(self, |cx| cx.consume_context::<T>()).expect("Runtime to exist")
|
||||
}
|
||||
|
||||
/// Consume context from the current scope
|
||||
@@ -651,7 +649,15 @@ impl ScopeId {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn throw_error(self, error: impl Into<CapturedError> + 'static) {
|
||||
throw_into(error, self)
|
||||
let error = error.into();
|
||||
if let Some(cx) = self.consume_context::<crate::ErrorContext>() {
|
||||
cx.insert_error(error)
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Tried to throw an error into an error boundary, but failed to locate a boundary: {:?}",
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the suspense context the current scope is in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
any_props::BoxedAnyProps, nodes::AsVNode, reactive_context::ReactiveContext,
|
||||
scope_context::Scope, Element, Runtime, VNode,
|
||||
any_props::BoxedAnyProps, reactive_context::ReactiveContext, scope_context::Scope, Element,
|
||||
RenderError, Runtime, VNode,
|
||||
};
|
||||
use std::{cell::Ref, rc::Rc};
|
||||
|
||||
@@ -76,11 +76,44 @@ pub struct ScopeState {
|
||||
pub(crate) context_id: ScopeId,
|
||||
/// The last node that has been rendered for this component. This node may not ben mounted
|
||||
/// During suspense, this component can be rendered in the background multiple times
|
||||
pub(crate) last_rendered_node: Option<Element>,
|
||||
pub(crate) last_rendered_node: Option<LastRenderedNode>,
|
||||
pub(crate) props: BoxedAnyProps,
|
||||
pub(crate) reactive_context: ReactiveContext,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum LastRenderedNode {
|
||||
Real(VNode),
|
||||
Placeholder(VNode, RenderError),
|
||||
}
|
||||
|
||||
impl std::ops::Deref for LastRenderedNode {
|
||||
type Target = VNode;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
LastRenderedNode::Real(vnode) => vnode,
|
||||
LastRenderedNode::Placeholder(vnode, _err) => vnode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LastRenderedNode {
|
||||
pub fn new(node: Element) -> Self {
|
||||
match node {
|
||||
Ok(vnode) => LastRenderedNode::Real(vnode),
|
||||
Err(err) => LastRenderedNode::Placeholder(VNode::placeholder(), err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vnode(&self) -> &VNode {
|
||||
match self {
|
||||
LastRenderedNode::Real(vnode) => vnode,
|
||||
LastRenderedNode::Placeholder(vnode, _err) => vnode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScopeState {
|
||||
fn drop(&mut self) {
|
||||
self.runtime.remove_scope(self.context_id);
|
||||
@@ -104,7 +137,11 @@ impl ScopeState {
|
||||
///
|
||||
/// Returns [`None`] if the tree has not been built yet.
|
||||
pub fn try_root_node(&self) -> Option<&VNode> {
|
||||
self.last_rendered_node.as_ref().map(AsVNode::as_vnode)
|
||||
match &self.last_rendered_node {
|
||||
Some(LastRenderedNode::Real(vnode)) => Some(vnode),
|
||||
Some(LastRenderedNode::Placeholder(vnode, _)) => Some(vnode),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the scope id of this [`ScopeState`].
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{innerlude::*, scope_context::SuspenseLocation};
|
||||
pub struct SuspenseBoundaryProps {
|
||||
fallback: Callback<SuspenseContext, Element>,
|
||||
/// The children of the suspense boundary
|
||||
children: Element,
|
||||
children: LastRenderedNode,
|
||||
}
|
||||
|
||||
impl Clone for SuspenseBoundaryProps {
|
||||
@@ -205,7 +205,10 @@ impl<__children: SuspenseBoundaryPropsBuilder_Optional<Element>>
|
||||
let fallback = fallback.0;
|
||||
let children = SuspenseBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
|
||||
SuspenseBoundaryPropsWithOwner {
|
||||
inner: SuspenseBoundaryProps { fallback, children },
|
||||
inner: SuspenseBoundaryProps {
|
||||
fallback,
|
||||
children: LastRenderedNode::new(children),
|
||||
},
|
||||
owner: self.owner,
|
||||
}
|
||||
}
|
||||
@@ -229,13 +232,7 @@ impl ::core::cmp::PartialEq for SuspenseBoundaryProps {
|
||||
/// fn App() -> Element {
|
||||
/// rsx! {
|
||||
/// SuspenseBoundary {
|
||||
/// fallback: |context: SuspenseContext| rsx! {
|
||||
/// if let Some(placeholder) = context.suspense_placeholder() {
|
||||
/// {placeholder}
|
||||
/// } else {
|
||||
/// "Loading..."
|
||||
/// }
|
||||
/// },
|
||||
/// fallback: |_| rsx! { "Loading..." },
|
||||
/// Article {}
|
||||
/// }
|
||||
/// }
|
||||
@@ -310,7 +307,7 @@ impl SuspenseBoundaryProps {
|
||||
|
||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.as_vnode().create(dom, parent, None::<&mut M>);
|
||||
children.create(dom, parent, None::<&mut M>);
|
||||
});
|
||||
|
||||
// Store the (now mounted) children back into the scope state
|
||||
@@ -325,6 +322,7 @@ impl SuspenseBoundaryProps {
|
||||
.suspense_context()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
// If there are suspended futures, render the fallback
|
||||
let nodes_created = if !suspense_context.suspended_futures().is_empty() {
|
||||
let (node, nodes_created) =
|
||||
@@ -337,9 +335,10 @@ impl SuspenseBoundaryProps {
|
||||
scope_id,
|
||||
)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(children.into());
|
||||
let suspense_placeholder = props.fallback.call(suspense_context);
|
||||
let nodes_created = suspense_placeholder.as_vnode().create(dom, parent, to);
|
||||
suspense_context.set_suspended_nodes(children.as_vnode().clone());
|
||||
let suspense_placeholder =
|
||||
LastRenderedNode::new(props.fallback.call(suspense_context));
|
||||
let nodes_created = suspense_placeholder.create(dom, parent, to);
|
||||
(suspense_placeholder, nodes_created)
|
||||
});
|
||||
|
||||
@@ -349,13 +348,11 @@ impl SuspenseBoundaryProps {
|
||||
nodes_created
|
||||
} else {
|
||||
// Otherwise just render the children in the real dom
|
||||
debug_assert!(children.as_vnode().mount.get().mounted());
|
||||
debug_assert!(children.mount.get().mounted());
|
||||
let nodes_created = suspense_context
|
||||
.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.as_vnode().create(dom, parent, to)
|
||||
});
|
||||
.under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to));
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
scope_state.last_rendered_node = Some(children);
|
||||
scope_state.last_rendered_node = children.into();
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
@@ -395,8 +392,8 @@ impl SuspenseBoundaryProps {
|
||||
suspense_context.inner.suspended_tasks.borrow_mut().clear();
|
||||
|
||||
// Get the parent of the suspense boundary to later create children with the right parent
|
||||
let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
|
||||
let mount = currently_rendered.as_vnode().mount.get();
|
||||
let currently_rendered = scope_state.last_rendered_node.clone().unwrap();
|
||||
let mount = currently_rendered.mount.get();
|
||||
let parent = {
|
||||
let mounts = dom.runtime.mounts.borrow();
|
||||
mounts
|
||||
@@ -412,24 +409,24 @@ impl SuspenseBoundaryProps {
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
|
||||
// Take the suspended nodes out of the suspense boundary so the children know that the boundary is not suspended while diffing
|
||||
let suspended = suspense_context.take_suspended_nodes();
|
||||
if let Some(node) = suspended {
|
||||
node.remove_node(&mut *dom, None::<&mut M>, None);
|
||||
}
|
||||
|
||||
// Replace the rendered nodes with resolved nodes
|
||||
currently_rendered
|
||||
.as_vnode()
|
||||
.remove_node(&mut *dom, Some(to), Some(replace_with));
|
||||
currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with));
|
||||
|
||||
// Switch to only writing templates
|
||||
only_write_templates(to);
|
||||
|
||||
children.as_vnode().mount.take();
|
||||
children.mount.take();
|
||||
|
||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.as_vnode().create(dom, parent, Some(to));
|
||||
children.create(dom, parent, Some(to));
|
||||
});
|
||||
|
||||
// Store the (now mounted) children back into the scope state
|
||||
@@ -454,7 +451,7 @@ impl SuspenseBoundaryProps {
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone();
|
||||
let last_rendered_node = scope.last_rendered_node.clone().unwrap();
|
||||
|
||||
let Self {
|
||||
fallback, children, ..
|
||||
@@ -467,19 +464,16 @@ impl SuspenseBoundaryProps {
|
||||
// We already have suspended nodes that still need to be suspended
|
||||
// Just diff the normal and suspended nodes
|
||||
(Some(suspended_nodes), true) => {
|
||||
let new_suspended_nodes: VNode = children.into();
|
||||
let new_suspended_nodes: VNode = children.as_vnode().clone();
|
||||
|
||||
// Diff the placeholder nodes in the dom
|
||||
let new_placeholder =
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
let old_placeholder = last_rendered_node;
|
||||
let new_placeholder = fallback.call(suspense_context.clone());
|
||||
let new_placeholder =
|
||||
LastRenderedNode::new(fallback.call(suspense_context.clone()));
|
||||
|
||||
old_placeholder.as_vnode().diff_node(
|
||||
new_placeholder.as_vnode(),
|
||||
dom,
|
||||
to,
|
||||
);
|
||||
old_placeholder.diff_node(&new_placeholder, dom, to);
|
||||
new_placeholder
|
||||
});
|
||||
|
||||
@@ -504,20 +498,19 @@ impl SuspenseBoundaryProps {
|
||||
let new_children = children;
|
||||
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_children
|
||||
.as_vnode()
|
||||
.diff_node(new_children.as_vnode(), dom, to);
|
||||
old_children.diff_node(&new_children, dom, to);
|
||||
});
|
||||
|
||||
// Set the last rendered node to the new children
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||
dom.scopes[scope_id.0].last_rendered_node = new_children.into();
|
||||
}
|
||||
// We have no suspended nodes, but we just became suspended. Move the children to the background
|
||||
(None, true) => {
|
||||
let old_children = last_rendered_node.as_vnode();
|
||||
let new_children: VNode = children.into();
|
||||
let old_children = last_rendered_node;
|
||||
let new_children: VNode = children.as_vnode().clone();
|
||||
|
||||
let new_placeholder = fallback.call(suspense_context.clone());
|
||||
let new_placeholder =
|
||||
LastRenderedNode::new(fallback.call(suspense_context.clone()));
|
||||
|
||||
// Move the children to the background
|
||||
let mount = old_children.mount.get();
|
||||
@@ -525,7 +518,7 @@ impl SuspenseBoundaryProps {
|
||||
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
old_children.move_node_to_background(
|
||||
std::slice::from_ref(new_placeholder.as_vnode()),
|
||||
std::slice::from_ref(&new_placeholder),
|
||||
parent,
|
||||
dom,
|
||||
to,
|
||||
@@ -558,13 +551,13 @@ impl SuspenseBoundaryProps {
|
||||
|
||||
// First diff the two children nodes in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_suspended_nodes.diff_node(new_children.as_vnode(), dom, None::<&mut M>);
|
||||
old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>);
|
||||
|
||||
// Then replace the placeholder with the new children
|
||||
let mount = old_placeholder.as_vnode().mount.get();
|
||||
let mount = old_placeholder.mount.get();
|
||||
let parent = dom.get_mounted_parent(mount);
|
||||
old_placeholder.as_vnode().replace(
|
||||
std::slice::from_ref(new_children.as_vnode()),
|
||||
old_placeholder.replace(
|
||||
std::slice::from_ref(&new_children),
|
||||
parent,
|
||||
dom,
|
||||
to,
|
||||
|
||||
@@ -37,50 +37,38 @@ use std::{
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct SuspendedFuture {
|
||||
origin: ScopeId,
|
||||
task: Task,
|
||||
pub(crate) placeholder: VNode,
|
||||
task: TaskId,
|
||||
}
|
||||
|
||||
impl SuspendedFuture {
|
||||
/// Create a new suspended future
|
||||
pub fn new(task: Task) -> Self {
|
||||
Self {
|
||||
task,
|
||||
task: task.id,
|
||||
origin: current_scope_id().unwrap_or_else(|e| panic!("{}", e)),
|
||||
placeholder: VNode::placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a placeholder to display while the future is suspended
|
||||
pub fn suspense_placeholder(&self) -> Option<VNode> {
|
||||
if self.placeholder == VNode::placeholder() {
|
||||
None
|
||||
} else {
|
||||
Some(self.placeholder.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a new placeholder the SuspenseBoundary may use to display while the future is suspended
|
||||
pub fn with_placeholder(mut self, placeholder: VNode) -> Self {
|
||||
self.placeholder = placeholder;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the task that was suspended
|
||||
pub fn task(&self) -> Task {
|
||||
self.task
|
||||
Task::from_id(self.task)
|
||||
}
|
||||
|
||||
/// Create a deep clone of this suspended future
|
||||
pub(crate) fn deep_clone(&self) -> Self {
|
||||
Self {
|
||||
task: self.task,
|
||||
placeholder: self.placeholder.deep_clone(),
|
||||
origin: self.origin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SuspendedFuture {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SuspendedFuture {{ task: {:?} }}", self.task)
|
||||
}
|
||||
}
|
||||
|
||||
/// A context with information about suspended components
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SuspenseContext {
|
||||
@@ -165,7 +153,7 @@ impl SuspenseContext {
|
||||
self.inner
|
||||
.suspended_tasks
|
||||
.borrow_mut()
|
||||
.retain(|t| t.task != task);
|
||||
.retain(|t| t.task != task.id);
|
||||
self.inner.id.get().needs_update();
|
||||
}
|
||||
|
||||
@@ -176,16 +164,6 @@ impl SuspenseContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the first suspended task with a loading placeholder
|
||||
pub fn suspense_placeholder(&self) -> Option<Element> {
|
||||
self.inner
|
||||
.suspended_tasks
|
||||
.borrow()
|
||||
.iter()
|
||||
.find_map(|task| task.suspense_placeholder())
|
||||
.map(std::result::Result::Ok)
|
||||
}
|
||||
|
||||
/// Run a closure after suspense is resolved
|
||||
pub fn after_suspense_resolved(&self, callback: impl FnOnce() + 'static) {
|
||||
let mut closures = self.inner.after_suspense_resolved.borrow_mut();
|
||||
@@ -226,37 +204,3 @@ impl Debug for SuspenseBoundaryInner {
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results
|
||||
///
|
||||
/// This trait is sealed and cannot be implemented outside of dioxus-core
|
||||
pub trait SuspenseExtension<T>: private::Sealed {
|
||||
/// Add a loading indicator if the result is suspended
|
||||
fn with_loading_placeholder(
|
||||
self,
|
||||
display_placeholder: impl FnOnce() -> Element,
|
||||
) -> std::result::Result<T, RenderError>;
|
||||
}
|
||||
|
||||
impl<T> SuspenseExtension<T> for std::result::Result<T, RenderError> {
|
||||
fn with_loading_placeholder(
|
||||
self,
|
||||
display_placeholder: impl FnOnce() -> Element,
|
||||
) -> std::result::Result<T, RenderError> {
|
||||
if let Err(RenderError::Suspended(suspense)) = self {
|
||||
Err(RenderError::Suspended(suspense.with_placeholder(
|
||||
display_placeholder().unwrap_or_default(),
|
||||
)))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl<T> Sealed for std::result::Result<T, RenderError> {}
|
||||
}
|
||||
@@ -20,11 +20,13 @@ use std::{pin::Pin, task::Poll};
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct Task {
|
||||
pub(crate) id: slotmap::DefaultKey,
|
||||
pub(crate) id: TaskId,
|
||||
// We add a raw pointer to make this !Send + !Sync
|
||||
unsend: PhantomData<*const ()>,
|
||||
}
|
||||
|
||||
pub(crate) type TaskId = slotmap::DefaultKey;
|
||||
|
||||
impl Task {
|
||||
/// Create a task from a raw id
|
||||
pub(crate) const fn from_id(id: slotmap::DefaultKey) -> Self {
|
||||
@@ -48,8 +50,6 @@ impl Task {
|
||||
}
|
||||
|
||||
/// Drop the task immediately.
|
||||
///
|
||||
/// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
|
||||
pub fn cancel(self) {
|
||||
remove_future(self);
|
||||
}
|
||||
@@ -382,6 +382,7 @@ impl TaskType {
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SchedulerMsg {
|
||||
/// All components have been marked as dirty, requiring a full render
|
||||
#[allow(unused)]
|
||||
AllDirty,
|
||||
|
||||
/// Immediate updates from Components that mark them as dirty
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//!
|
||||
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
|
||||
|
||||
use crate::innerlude::Work;
|
||||
use crate::properties::RootProps;
|
||||
use crate::root_wrapper::RootScopeWrapper;
|
||||
use crate::{
|
||||
@@ -12,6 +11,7 @@ use crate::{
|
||||
scopes::ScopeId,
|
||||
ComponentFunction, Element, Mutations,
|
||||
};
|
||||
use crate::{innerlude::Work, scopes::LastRenderedNode};
|
||||
use crate::{Task, VComponent};
|
||||
use futures_util::StreamExt;
|
||||
use slab::Slab;
|
||||
@@ -351,6 +351,11 @@ impl VirtualDom {
|
||||
f()
|
||||
}
|
||||
|
||||
/// Run a closure inside a specific scope
|
||||
pub fn in_scope<T>(&self, scope: ScopeId, f: impl FnOnce() -> T) -> T {
|
||||
self.in_runtime(|| scope.in_runtime(f))
|
||||
}
|
||||
|
||||
/// Build the virtualdom with a global context inserted into the base scope
|
||||
///
|
||||
/// This is useful for what is essentially dependency injection when building the app
|
||||
@@ -578,6 +583,8 @@ impl VirtualDom {
|
||||
.clone()
|
||||
.while_rendering(|| self.run_scope(ScopeId::ROOT));
|
||||
|
||||
let new_nodes = LastRenderedNode::new(new_nodes);
|
||||
|
||||
self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
|
||||
|
||||
// Rebuilding implies we append the created elements to the root
|
||||
|
||||
@@ -12,7 +12,7 @@ rust-version = "1.76.0"
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize", "mounted", "file_engine"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize"] }
|
||||
dioxus-document = { workspace = true }
|
||||
dioxus-signals = { workspace = true, optional = true }
|
||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
|
||||
@@ -50,6 +50,7 @@ base64 = { workspace = true }
|
||||
libc = "0.2.174"
|
||||
rand = { workspace = true, features = ["std_rng"] }
|
||||
subtle = { version = "2.6", features = ["const-generics"] }
|
||||
bytes = { workspace = true }
|
||||
webbrowser = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
config::{Config, WindowCloseBehaviour},
|
||||
edits::EditWebsocket,
|
||||
event_handlers::WindowEventHandlers,
|
||||
file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine},
|
||||
file_upload::{DesktopFileUploadForm, FileDialogRequest},
|
||||
ipc::{IpcMessage, UserWindowEvent},
|
||||
query::QueryResult,
|
||||
shortcut::ShortcutRegistry,
|
||||
@@ -16,7 +16,6 @@ use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tao::{
|
||||
@@ -420,9 +419,7 @@ impl App {
|
||||
let event_bubbles = file_dialog.bubbles;
|
||||
let files = file_dialog.get_file_event();
|
||||
|
||||
let as_any = Box::new(DesktopFileUploadForm {
|
||||
files: Arc::new(NativeFileEngine::new(files)),
|
||||
});
|
||||
let as_any = Box::new(DesktopFileUploadForm { files });
|
||||
|
||||
let data = Rc::new(PlatformEventData::new(as_any));
|
||||
|
||||
|
||||
@@ -354,7 +354,7 @@ fn is_main_thread() -> bool {
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct PendingDesktopContext {
|
||||
pub(crate) receiver: tokio::sync::oneshot::Receiver<DesktopContext>,
|
||||
pub(crate) receiver: futures_channel::oneshot::Receiver<DesktopContext>,
|
||||
}
|
||||
|
||||
impl PendingDesktopContext {
|
||||
@@ -366,9 +366,7 @@ impl PendingDesktopContext {
|
||||
}
|
||||
|
||||
/// Try to resolve the pending context into a [`DesktopContext`].
|
||||
pub async fn try_resolve(
|
||||
self,
|
||||
) -> Result<DesktopContext, tokio::sync::oneshot::error::RecvError> {
|
||||
pub async fn try_resolve(self) -> Result<DesktopContext, futures_channel::oneshot::Canceled> {
|
||||
self.receiver.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use dioxus_html::{
|
||||
point_interaction::{
|
||||
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
|
||||
},
|
||||
FileEngine, HasDragData, HasFileData, HasFormData, HasMouseData, SerializedMouseData,
|
||||
SerializedPointInteraction,
|
||||
FileData, HasDragData, HasFileData, HasFormData, HasMouseData, NativeFileData,
|
||||
SerializedMouseData, SerializedPointInteraction,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
@@ -155,12 +155,16 @@ impl FromStr for Filters {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DesktopFileUploadForm {
|
||||
pub files: Arc<NativeFileEngine>,
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl HasFileData for DesktopFileUploadForm {
|
||||
fn files(&self) -> Option<Arc<dyn FileEngine>> {
|
||||
Some(self.files.clone())
|
||||
fn files(&self) -> Vec<FileData> {
|
||||
self.files
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|f| FileData::new(DesktopFileData(f)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,12 +191,16 @@ impl NativeFileHover {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DesktopFileDragEvent {
|
||||
pub mouse: SerializedPointInteraction,
|
||||
pub files: Arc<NativeFileEngine>,
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl HasFileData for DesktopFileDragEvent {
|
||||
fn files(&self) -> Option<Arc<dyn FileEngine>> {
|
||||
Some(self.files.clone())
|
||||
fn files(&self) -> Vec<FileData> {
|
||||
self.files
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|f| FileData::new(DesktopFileData(f)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,78 +256,93 @@ impl PointerInteraction for DesktopFileDragEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NativeFileEngine {
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct DesktopFileData(pub(crate) PathBuf);
|
||||
|
||||
impl NativeFileEngine {
|
||||
pub fn new(files: Vec<PathBuf>) -> Self {
|
||||
Self { files }
|
||||
impl NativeFileData for DesktopFileData {
|
||||
fn name(&self) -> String {
|
||||
self.0.file_name().unwrap().to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl FileEngine for NativeFileEngine {
|
||||
fn files(&self) -> Vec<String> {
|
||||
self.files
|
||||
.iter()
|
||||
.filter_map(|f| Some(f.to_str()?.to_string()))
|
||||
.collect()
|
||||
fn size(&self) -> u64 {
|
||||
std::fs::metadata(&self.0).map(|m| m.len()).unwrap_or(0)
|
||||
}
|
||||
|
||||
async fn file_size(&self, file: &str) -> Option<u64> {
|
||||
#[cfg(feature = "tokio_runtime")]
|
||||
{
|
||||
let file = File::open(file).await.ok()?;
|
||||
Some(file.metadata().await.ok()?.len())
|
||||
}
|
||||
#[cfg(not(feature = "tokio_runtime"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
fn last_modified(&self) -> u64 {
|
||||
std::fs::metadata(&self.0)
|
||||
.and_then(|m| m.modified())
|
||||
.ok()
|
||||
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
|
||||
#[cfg(feature = "tokio_runtime")]
|
||||
{
|
||||
let mut file = File::open(file).await.ok()?;
|
||||
fn read_bytes(
|
||||
&self,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = Result<bytes::Bytes, dioxus_core::Error>> + 'static>,
|
||||
> {
|
||||
let path = self.0.clone();
|
||||
Box::pin(async move { Ok(bytes::Bytes::from(std::fs::read(&path)?)) })
|
||||
}
|
||||
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents).await.ok()?;
|
||||
fn read_string(
|
||||
&self,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = Result<String, dioxus_core::Error>> + 'static>,
|
||||
> {
|
||||
let path = self.0.clone();
|
||||
Box::pin(async move { Ok(std::fs::read_to_string(&path)?) })
|
||||
}
|
||||
|
||||
Some(contents)
|
||||
}
|
||||
#[cfg(not(feature = "tokio_runtime"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
fn inner(&self) -> &dyn std::any::Any {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
async fn read_file_to_string(&self, file: &str) -> Option<String> {
|
||||
fn byte_stream(
|
||||
&self,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn futures_util::Stream<Item = Result<bytes::Bytes, dioxus_core::Error>>
|
||||
+ 'static
|
||||
+ Send,
|
||||
>,
|
||||
> {
|
||||
let path = self.0.clone();
|
||||
#[cfg(feature = "tokio_runtime")]
|
||||
{
|
||||
let mut file = File::open(file).await.ok()?;
|
||||
// todo!()
|
||||
// use futures_util::TryFutureExt;
|
||||
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents).await.ok()?;
|
||||
|
||||
Some(contents)
|
||||
}
|
||||
#[cfg(not(feature = "tokio_runtime"))]
|
||||
{
|
||||
None
|
||||
// futures_util::stream::try_unfold(File::open(path), |mut file| async move {
|
||||
// let mut buf = vec![0; 8192];
|
||||
// let n = file
|
||||
// .read(&mut buf)
|
||||
// .await
|
||||
// .map_err(|e| dioxus_core::Error::from(e))?;
|
||||
// if n == 0 {
|
||||
// Ok(None)
|
||||
// } else {
|
||||
// buf.truncate(n);
|
||||
// Ok(Some((bytes::Bytes::from(buf), file)))
|
||||
// }
|
||||
// })
|
||||
// .map(|res| res.map(bytes::Bytes::from))
|
||||
// .boxed()
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
|
||||
#[cfg(feature = "tokio_runtime")]
|
||||
{
|
||||
let file = File::open(file).await.ok()?;
|
||||
Some(Box::new(file))
|
||||
}
|
||||
#[cfg(not(feature = "tokio_runtime"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
fn content_type(&self) -> Option<String> {
|
||||
Some(
|
||||
dioxus_asset_resolver::native::get_mime_from_ext(
|
||||
self.0.extension().and_then(|ext| ext.to_str()),
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,9 @@ use crate::menubar::DioxusMenu;
|
||||
use crate::PendingDesktopContext;
|
||||
use crate::WindowCloseBehaviour;
|
||||
use crate::{
|
||||
app::SharedContext,
|
||||
assets::AssetHandlerRegistry,
|
||||
edits::WryQueue,
|
||||
file_upload::{NativeFileEngine, NativeFileHover},
|
||||
ipc::UserWindowEvent,
|
||||
protocol,
|
||||
waker::tao_waker,
|
||||
Config, DesktopContext, DesktopService,
|
||||
app::SharedContext, assets::AssetHandlerRegistry, edits::WryQueue,
|
||||
file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
|
||||
DesktopContext, DesktopService,
|
||||
};
|
||||
use crate::{document::DesktopDocument, WeakDesktopContext};
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
@@ -19,10 +14,9 @@ use dioxus_core::{Runtime, ScopeId, VirtualDom};
|
||||
use dioxus_document::Document;
|
||||
use dioxus_history::{History, MemoryHistory};
|
||||
use dioxus_hooks::to_owned;
|
||||
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
|
||||
use dioxus_html::{HtmlEvent, PlatformEventData};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::{cell::OnceCell, time::Duration};
|
||||
use std::{rc::Rc, task::Waker};
|
||||
use wry::{DragDropEvent, RequestAsyncResponder, WebContext, WebViewBuilder, WebViewId};
|
||||
@@ -90,7 +84,7 @@ impl WebviewEdits {
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Error parsing user_event: {:?}.Contents: {:?}, raw: {:#?}",
|
||||
"Error parsing user_event: {:?}. \n Contents: {:?}, \nraw: {:#?}",
|
||||
err,
|
||||
String::from_utf8(request.body().to_vec()),
|
||||
request
|
||||
@@ -131,20 +125,17 @@ impl WebviewEdits {
|
||||
}
|
||||
dioxus_html::EventData::Drag(ref drag) => {
|
||||
// we want to override this with a native file engine, provided by the most recent drag event
|
||||
if drag.files().is_some() {
|
||||
let file_event = recent_file.current().unwrap();
|
||||
let paths = match file_event {
|
||||
wry::DragDropEvent::Enter { paths, .. } => paths,
|
||||
wry::DragDropEvent::Drop { paths, .. } => paths,
|
||||
_ => vec![],
|
||||
};
|
||||
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
|
||||
mouse: drag.mouse.clone(),
|
||||
files: Arc::new(NativeFileEngine::new(paths)),
|
||||
})))
|
||||
} else {
|
||||
data.into_any()
|
||||
}
|
||||
let file_event = recent_file.current().unwrap();
|
||||
let paths = match file_event {
|
||||
wry::DragDropEvent::Enter { paths, .. } => paths,
|
||||
wry::DragDropEvent::Drop { paths, .. } => paths,
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
|
||||
mouse: drag.mouse.clone(),
|
||||
files: paths,
|
||||
})))
|
||||
}
|
||||
_ => data.into_any(),
|
||||
};
|
||||
@@ -561,12 +552,12 @@ impl SynchronousEventResponse {
|
||||
pub(crate) struct PendingWebview {
|
||||
dom: VirtualDom,
|
||||
cfg: Config,
|
||||
sender: tokio::sync::oneshot::Sender<DesktopContext>,
|
||||
sender: futures_channel::oneshot::Sender<DesktopContext>,
|
||||
}
|
||||
|
||||
impl PendingWebview {
|
||||
pub(crate) fn new(dom: VirtualDom, cfg: Config) -> (Self, PendingDesktopContext) {
|
||||
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||
let (sender, receiver) = futures_channel::oneshot::channel();
|
||||
let webview = Self { dom, cfg, sender };
|
||||
let pending = PendingDesktopContext { receiver };
|
||||
(webview, pending)
|
||||
|
||||
+20
-14
@@ -24,17 +24,17 @@ dioxus-signals = { workspace = true, optional = true }
|
||||
dioxus-router = { workspace = true, optional = true }
|
||||
dioxus-web = { workspace = true, default-features = false, optional = true }
|
||||
dioxus-desktop = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-fullstack = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-liveview = { workspace = true, optional = true }
|
||||
dioxus-server = { workspace = true, optional = true }
|
||||
dioxus-ssr = { workspace = true, optional = true }
|
||||
dioxus-native = { workspace = true, optional = true }
|
||||
dioxus_server_macro = { workspace = true, optional = true }
|
||||
dioxus-server = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-fullstack = { workspace = true, default-features = true, optional = true }
|
||||
dioxus-fullstack-macro = { workspace = true, optional = true }
|
||||
dioxus-asset-resolver = { workspace = true, optional = true }
|
||||
manganis = { workspace = true, features = ["dioxus"], optional = true }
|
||||
dioxus-logger = { workspace = true, optional = true }
|
||||
warnings = { workspace = true, optional = true }
|
||||
wasm-splitter = { workspace = true, package = "wasm-split", optional = true }
|
||||
wasm-splitter = { workspace = true, optional = true }
|
||||
subsecond = { workspace = true }
|
||||
|
||||
serde = { workspace = true, optional = true }
|
||||
@@ -55,7 +55,6 @@ lib = [
|
||||
"signals",
|
||||
"hooks",
|
||||
"mounted",
|
||||
"file_engine",
|
||||
"document",
|
||||
"asset",
|
||||
"warnings",
|
||||
@@ -67,9 +66,8 @@ signals = ["dep:dioxus-signals", "dep:dioxus-stores"]
|
||||
macro = ["dep:dioxus-core-macro"]
|
||||
html = ["dep:dioxus-html"]
|
||||
hooks = ["dep:dioxus-hooks"]
|
||||
devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/devtools"]
|
||||
devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools"]
|
||||
mounted = ["dioxus-web?/mounted"]
|
||||
file_engine = ["dioxus-web?/file_engine"]
|
||||
asset = ["dep:manganis", "dep:dioxus-asset-resolver"]
|
||||
document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
|
||||
logger = ["dep:dioxus-logger"]
|
||||
@@ -84,9 +82,19 @@ launch = ["dep:dioxus-config-macro"]
|
||||
router = ["dep:dioxus-router"]
|
||||
|
||||
# Platforms
|
||||
fullstack = ["dep:dioxus-fullstack", "dioxus-config-macro/fullstack", "dep:serde"]
|
||||
desktop = ["dep:dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"]
|
||||
mobile = ["dep:dioxus-desktop", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
|
||||
fullstack = [
|
||||
"dep:dioxus-fullstack",
|
||||
"dioxus-config-macro/fullstack",
|
||||
"dep:serde",
|
||||
"dioxus-web?/document",
|
||||
"dioxus-web?/hydrate",
|
||||
"dioxus-server?/document",
|
||||
"dioxus-web?/devtools",
|
||||
"dioxus-web?/mounted",
|
||||
"dioxus-web?/document"
|
||||
]
|
||||
desktop = ["dep:dioxus-desktop", "dioxus-config-macro/desktop"]
|
||||
mobile = ["dep:dioxus-desktop", "dioxus-config-macro/mobile"]
|
||||
web = [
|
||||
"dep:dioxus-web",
|
||||
"dioxus-fullstack?/web",
|
||||
@@ -100,12 +108,10 @@ liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
|
||||
native = ["dep:dioxus-native"] # todo(jon): decompose the desktop crate such that "webview" is the default and native is opt-in
|
||||
server = [
|
||||
"dep:dioxus-server",
|
||||
"dep:dioxus_server_macro",
|
||||
"dioxus_server_macro/server",
|
||||
"dioxus_server_macro/axum",
|
||||
"dioxus-fullstack?/server",
|
||||
"dep:dioxus-fullstack-macro",
|
||||
"ssr",
|
||||
"dioxus-liveview?/axum",
|
||||
"dioxus-fullstack?/server",
|
||||
]
|
||||
|
||||
# This feature just disables the no-renderer-enabled warning
|
||||
|
||||
@@ -40,9 +40,12 @@ use crate::prelude::*;
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus::launch(app);
|
||||
/// }
|
||||
///
|
||||
/// dioxus::launch(app);
|
||||
/// fn app() -> Element {
|
||||
/// rsx! {
|
||||
/// div { "Hello, world!" }
|
||||
@@ -299,7 +302,7 @@ impl LaunchBuilder {
|
||||
// Set any flags if we're running under fullstack
|
||||
#[cfg(feature = "fullstack")]
|
||||
{
|
||||
use dioxus_fullstack::server_fn::client::{get_server_url, set_server_url};
|
||||
use dioxus_fullstack::{get_server_url, set_server_url};
|
||||
|
||||
// Make sure to set the server_fn endpoint if the user specified the fullstack feature
|
||||
// We only set this on native targets
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
pub use dioxus_core;
|
||||
#[doc(inline)]
|
||||
pub use dioxus_core::{CapturedError, Ok, Result};
|
||||
pub use dioxus_core::{CapturedError, Error, Ok, Result};
|
||||
|
||||
#[cfg(feature = "launch")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||
@@ -85,6 +85,9 @@ pub use dioxus_cli_config as cli_config;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
pub use dioxus_server as server;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub use dioxus_server::serve;
|
||||
|
||||
#[cfg(feature = "devtools")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "devtools")))]
|
||||
pub use dioxus_devtools as devtools;
|
||||
@@ -158,7 +161,7 @@ pub mod prelude {
|
||||
#[cfg(feature = "signals")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "signals")))]
|
||||
#[doc(inline)]
|
||||
pub use dioxus_signals::*;
|
||||
pub use dioxus_signals::{self, *};
|
||||
|
||||
#[cfg(feature = "signals")]
|
||||
pub use dioxus_stores::{self, store, use_store, GlobalStore, ReadStore, Store, WriteStore};
|
||||
@@ -200,14 +203,16 @@ pub mod prelude {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "fullstack")))]
|
||||
#[doc(inline)]
|
||||
pub use dioxus_fullstack::{
|
||||
server, server_fn, use_server_cached, use_server_future, ServerFnError, ServerFnResult,
|
||||
self as dioxus_fullstack, delete, get, patch, post, put, server, use_loader,
|
||||
use_server_cached, use_server_future, HttpError, OrHttpError, ServerFnError,
|
||||
ServerFnResult,
|
||||
};
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||
#[doc(inline)]
|
||||
pub use dioxus_server::{
|
||||
extract, DioxusRouterExt, DioxusRouterFnExt, FromContext, ServeConfig,
|
||||
self, serve, DioxusRouterExt, DioxusRouterFnExt, ServeConfig, ServerFunction,
|
||||
};
|
||||
|
||||
#[cfg(feature = "router")]
|
||||
@@ -233,9 +238,12 @@ pub mod prelude {
|
||||
|
||||
#[doc(inline)]
|
||||
pub use dioxus_core::{
|
||||
consume_context, provide_context, spawn, suspend, try_consume_context, use_hook, Attribute,
|
||||
Callback, Component, Context, Element, ErrorBoundary, ErrorContext, Event, EventHandler,
|
||||
Fragment, HasAttributes, IntoDynNode, RenderError, ScopeId, SuspenseBoundary,
|
||||
SuspenseContext, SuspenseExtension, VNode, VirtualDom,
|
||||
consume_context, provide_context, spawn, suspend, try_consume_context, use_hook,
|
||||
AnyhowContext, Attribute, Callback, Component, Element, ErrorBoundary, ErrorContext, Event,
|
||||
EventHandler, Fragment, HasAttributes, IntoDynNode, RenderError, Result, ScopeId,
|
||||
SuspenseBoundary, SuspenseContext, VNode, VirtualDom,
|
||||
};
|
||||
|
||||
#[cfg(feature = "logger")]
|
||||
pub use dioxus_logger::tracing::{debug, error, info, trace, warn};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
/// Render a [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link) tag into the head of the page with the stylesheet rel.
|
||||
/// This is equivalent to the [`Link`](crate::Link) component with a slightly more ergonomic API.
|
||||
/// This is equivalent to the [`Link`](Link) component with a slightly more ergonomic API.
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "dioxus-fullstack-hooks"
|
||||
name = "dioxus-fullstack-core"
|
||||
authors = ["Jonathan Kelley", "Evan Almloff"]
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
@@ -11,23 +11,32 @@ keywords = ["web", "gui", "server"]
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { workspace = true }
|
||||
dioxus-core = { workspace = true, features = ["serialize"]}
|
||||
dioxus-signals = { workspace = true }
|
||||
dioxus-hooks = { workspace = true }
|
||||
dioxus-fullstack-protocol = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
dioxus-history.workspace = true
|
||||
dioxus-document.workspace = true
|
||||
futures-util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
dioxus-history = { workspace = true }
|
||||
dioxus-document = { workspace = true }
|
||||
ciborium = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
axum-core = { workspace = true }
|
||||
http = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
generational-box = { workspace = true }
|
||||
futures-util = { workspace = true, features = ["std"] }
|
||||
|
||||
[features]
|
||||
web = []
|
||||
server = []
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dioxus Fullstack Hooks
|
||||
# Dioxus Fullstack Core
|
||||
|
||||
Dioxus fullstack hooks provides hooks and contexts for [`dioxus-fullstack`](https://crates.io/crates/dioxus-fullstack). Libraries that need to integrate with dioxus-fullstack should rely on this crate instead of the renderer for quicker build times.
|
||||
Dioxus-fullstack-core provides types, traits, hooks, contexts for [`dioxus-fullstack`](https://crates.io/crates/dioxus-fullstack). Libraries that need to integrate with dioxus-fullstack should rely on this crate instead of the full-fledged renderer for quicker build times.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -14,12 +14,11 @@ Then you can use hooks like `use_server_future` in your components:
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
async fn fetch_article(id: u32) -> String {
|
||||
format!("Article {}", id)
|
||||
}
|
||||
|
||||
|
||||
fn App() -> Element {
|
||||
let mut article_id = use_signal(|| 0);
|
||||
|
||||
// `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||
// The future will rerun any time the
|
||||
// Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||
@@ -29,4 +28,8 @@ fn App() -> Element {
|
||||
"{article().unwrap()}"
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_article(id: u32) -> String {
|
||||
format!("Article {}", id)
|
||||
}
|
||||
```
|
||||
@@ -3,7 +3,7 @@
|
||||
use dioxus_document::*;
|
||||
|
||||
fn head_element_written_on_server() -> bool {
|
||||
dioxus_fullstack_protocol::head_element_hydration_entry()
|
||||
crate::transport::head_element_hydration_entry()
|
||||
.get()
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
@@ -0,0 +1,223 @@
|
||||
use axum_core::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::HttpError;
|
||||
|
||||
/// A default result type for server functions, which can either be successful or contain an error. The [`ServerFnResult`] type
|
||||
/// is a convenient alias for a `Result` type that uses [`ServerFnError`] as the error type.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// #[server]
|
||||
/// async fn parse_number(number: String) -> Result<f32> {
|
||||
/// let parsed_number: f32 = number.parse()?;
|
||||
/// Ok(parsed_number)
|
||||
/// }
|
||||
/// ```
|
||||
pub type ServerFnResult<T = ()> = std::result::Result<T, ServerFnError>;
|
||||
|
||||
/// The error type for the server function system. This enum encompasses all possible errors that can occur
|
||||
/// during the registration, invocation, and processing of server functions.
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServerFnError {
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
///
|
||||
/// The `details` field can optionally contain additional structured information about the error.
|
||||
/// When passing typed errors from the server to the client, the `details` field contains the serialized
|
||||
/// representation of the error.
|
||||
#[error("error running server function: {message} (details: {details:#?})")]
|
||||
ServerError {
|
||||
message: String,
|
||||
|
||||
/// Optional HTTP status code associated with the error.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
code: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
details: Option<serde_json::Value>,
|
||||
},
|
||||
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
#[error("error reaching server to call server function: {0} ")]
|
||||
Request(RequestError),
|
||||
|
||||
/// Occurs on the client if there is an error while trying to read the response body as a stream.
|
||||
#[error("error reading response body stream: {0}")]
|
||||
StreamError(String),
|
||||
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
#[error("error while trying to register the server function: {0}")]
|
||||
Registration(String),
|
||||
|
||||
/// Occurs on the client if trying to use an unsupported `HTTP` method when building a request.
|
||||
#[error("error trying to build `HTTP` method request: {0}")]
|
||||
UnsupportedRequestMethod(String),
|
||||
|
||||
/// Occurs when there is an error while actually running the middleware on the server.
|
||||
#[error("error running middleware: {0}")]
|
||||
MiddlewareError(String),
|
||||
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
#[error("error deserializing server function results: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
#[error("error serializing server function arguments: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
#[error("error deserializing server function arguments: {0}")]
|
||||
Args(String),
|
||||
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
#[error("missing argument {0}")]
|
||||
MissingArg(String),
|
||||
|
||||
/// Occurs on the server if there is an error creating an HTTP response.
|
||||
#[error("error creating response {0}")]
|
||||
Response(String),
|
||||
}
|
||||
|
||||
impl ServerFnError {
|
||||
/// Create a new server error (status code 500) with a message.
|
||||
pub fn new(f: impl ToString) -> Self {
|
||||
ServerFnError::ServerError {
|
||||
message: f.to_string(),
|
||||
details: None,
|
||||
code: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ServerFnError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
ServerFnError::ServerError {
|
||||
message: value.to_string(),
|
||||
details: None,
|
||||
code: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ServerFnError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
ServerFnError::Deserialization(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerFnError> for http::StatusCode {
|
||||
fn from(value: ServerFnError) -> Self {
|
||||
match value {
|
||||
ServerFnError::ServerError { code, .. } => match code {
|
||||
Some(code) => http::StatusCode::from_u16(code)
|
||||
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
None => http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
ServerFnError::Request(err) => match err {
|
||||
RequestError::Status(_, code) => http::StatusCode::from_u16(code)
|
||||
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
_ => http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
ServerFnError::StreamError(_)
|
||||
| ServerFnError::Registration(_)
|
||||
| ServerFnError::UnsupportedRequestMethod(_)
|
||||
| ServerFnError::MiddlewareError(_)
|
||||
| ServerFnError::Deserialization(_)
|
||||
| ServerFnError::Serialization(_)
|
||||
| ServerFnError::Args(_)
|
||||
| ServerFnError::MissingArg(_)
|
||||
| ServerFnError::Response(_) => http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestError> for ServerFnError {
|
||||
fn from(value: RequestError) -> Self {
|
||||
ServerFnError::Request(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for ServerFnError {
|
||||
fn from(value: HttpError) -> Self {
|
||||
ServerFnError::ServerError {
|
||||
message: value.message.unwrap_or_else(|| {
|
||||
value
|
||||
.status
|
||||
.canonical_reason()
|
||||
.unwrap_or("Unknown error")
|
||||
.to_string()
|
||||
}),
|
||||
code: Some(value.status.as_u16()),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ServerFnError {
|
||||
fn into_response(self) -> axum_core::response::Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error type representing issues that can occur while making requests.
|
||||
///
|
||||
/// This is made to paper over the reqwest::Error type which we don't want to export here and
|
||||
/// is limited in many ways.
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RequestError {
|
||||
/// An error occurred when building the request.
|
||||
#[error("error building request: {0}")]
|
||||
Builder(String),
|
||||
|
||||
/// An error occurred when serializing the request body.
|
||||
#[error("error serializing request body: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// An error occurred when following a redirect.
|
||||
#[error("error following redirect: {0}")]
|
||||
Redirect(String),
|
||||
|
||||
/// An error occurred when receiving a non-2xx status code.
|
||||
#[error("error receiving status code: {0} ({1})")]
|
||||
Status(String, u16),
|
||||
|
||||
/// An error occurred when a request times out.
|
||||
#[error("error timing out: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
/// An error occurred when sending a request.
|
||||
#[error("error sending request: {0}")]
|
||||
Request(String),
|
||||
|
||||
/// An error occurred when upgrading a connection.
|
||||
#[error("error upgrading connection: {0}")]
|
||||
Connect(String),
|
||||
|
||||
/// An error occurred when there is a request or response body error.
|
||||
#[error("request or response body error: {0}")]
|
||||
Body(String),
|
||||
|
||||
/// An error occurred when decoding the response body.
|
||||
#[error("error decoding response body: {0}")]
|
||||
Decode(String),
|
||||
}
|
||||
|
||||
impl RequestError {
|
||||
pub fn status(&self) -> Option<StatusCode> {
|
||||
match self {
|
||||
RequestError::Status(_, code) => Some(StatusCode::from_u16(*code).ok()?),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
RequestError::Status(_, code) => Some(*code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
//! Contains a hydration compatible error boundary context.
|
||||
|
||||
use crate::serialize_context;
|
||||
use dioxus_core::{spawn_isomorphic, CapturedError, ErrorContext, ReactiveContext};
|
||||
use dioxus_fullstack_protocol::serialize_context;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
/// Initializes an error boundary context that is compatible with hydration.
|
||||
pub fn init_error_boundary() -> ErrorContext {
|
||||
let initial_errors = serialize_context().create_entry::<Vec<CapturedError>>();
|
||||
let initial_error = serialize_context().create_entry::<Option<CapturedError>>();
|
||||
let (rx_context, mut rx) = ReactiveContext::new();
|
||||
let errors = ErrorContext::new(Vec::new());
|
||||
for error in initial_errors.get().iter().flatten() {
|
||||
errors.insert_error(error.clone());
|
||||
let errors = ErrorContext::new(None);
|
||||
if let Ok(Some(err)) = initial_error.get() {
|
||||
errors.insert_error(err);
|
||||
}
|
||||
rx_context.run_in(|| {
|
||||
errors.errors();
|
||||
errors.error();
|
||||
});
|
||||
spawn_isomorphic({
|
||||
let errors = errors.clone();
|
||||
async move {
|
||||
if rx.next().await.is_some() {
|
||||
rx_context.run_in(|| {
|
||||
initial_errors.insert(&errors.errors().to_vec(), std::panic::Location::caller())
|
||||
initial_error.insert(&errors.error(), std::panic::Location::caller())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::transport::{is_hydrating, SerializeContextEntry};
|
||||
use dioxus_core::{provide_context, queue_effect, schedule_update, try_consume_context};
|
||||
use dioxus_fullstack_protocol::{is_hydrating, SerializeContextEntry};
|
||||
use dioxus_history::{history, provide_history_context, History};
|
||||
|
||||
// If we are currently in a scope and this is the first run then queue a rerender
|
||||
@@ -32,18 +32,18 @@ pub(crate) fn finalize_route() {
|
||||
let Some(entry) = try_consume_context::<RouteEntry>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entry = entry
|
||||
.entry
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.expect("Failed to get initial route from hydration context");
|
||||
|
||||
if cfg!(feature = "server") {
|
||||
let history = history();
|
||||
let initial_route = history.current_route();
|
||||
entry.insert(&initial_route, std::panic::Location::caller());
|
||||
provide_context(ResolvedRouteContext {
|
||||
route: initial_route,
|
||||
});
|
||||
let route = history.current_route();
|
||||
entry.insert(&route, std::panic::Location::caller());
|
||||
provide_context(ResolvedRouteContext { route });
|
||||
} else if cfg!(feature = "web") {
|
||||
let initial_route = entry
|
||||
.get()
|
||||
@@ -57,7 +57,7 @@ pub(crate) fn finalize_route() {
|
||||
/// Provide the fullstack history context. This interacts with the hydration context so it must
|
||||
/// be called in the same order on the client and server after the hydration context is created
|
||||
pub fn provide_fullstack_history_context<H: History + 'static>(history: H) {
|
||||
let entry = dioxus_fullstack_protocol::serialize_context().create_entry();
|
||||
let entry = crate::transport::serialize_context().create_entry();
|
||||
provide_context(RouteEntry {
|
||||
entry: Rc::new(RefCell::new(Some(entry.clone()))),
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
use axum_core::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use std::fmt;
|
||||
|
||||
/// An error type that wraps an HTTP status code and optional message.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HttpError {
|
||||
pub status: StatusCode,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl HttpError {
|
||||
pub fn new<M: Into<String>>(status: StatusCode, message: M) -> Self {
|
||||
HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn err<T>(status: StatusCode, message: impl Into<String>) -> Result<T, Self> {
|
||||
Err(HttpError::new(status, message))
|
||||
}
|
||||
|
||||
// --- 4xx Client Errors ---
|
||||
pub fn bad_request<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::BAD_REQUEST, message)
|
||||
}
|
||||
pub fn unauthorized<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::UNAUTHORIZED, message)
|
||||
}
|
||||
pub fn payment_required<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PAYMENT_REQUIRED, message)
|
||||
}
|
||||
pub fn forbidden<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::FORBIDDEN, message)
|
||||
}
|
||||
pub fn not_found<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_FOUND, message)
|
||||
}
|
||||
pub fn method_not_allowed<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::METHOD_NOT_ALLOWED, message)
|
||||
}
|
||||
pub fn not_acceptable<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_ACCEPTABLE, message)
|
||||
}
|
||||
pub fn proxy_auth_required<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PROXY_AUTHENTICATION_REQUIRED, message)
|
||||
}
|
||||
pub fn request_timeout<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::REQUEST_TIMEOUT, message)
|
||||
}
|
||||
pub fn conflict<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::CONFLICT, message)
|
||||
}
|
||||
pub fn gone<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::GONE, message)
|
||||
}
|
||||
pub fn length_required<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::LENGTH_REQUIRED, message)
|
||||
}
|
||||
pub fn precondition_failed<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PRECONDITION_FAILED, message)
|
||||
}
|
||||
pub fn payload_too_large<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PAYLOAD_TOO_LARGE, message)
|
||||
}
|
||||
pub fn uri_too_long<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::URI_TOO_LONG, message)
|
||||
}
|
||||
pub fn unsupported_media_type<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
|
||||
}
|
||||
pub fn im_a_teapot<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::IM_A_TEAPOT, message)
|
||||
}
|
||||
pub fn too_many_requests<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::TOO_MANY_REQUESTS, message)
|
||||
}
|
||||
|
||||
// --- 5xx Server Errors ---
|
||||
pub fn internal_server_error<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::INTERNAL_SERVER_ERROR, message)
|
||||
}
|
||||
pub fn not_implemented<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_IMPLEMENTED, message)
|
||||
}
|
||||
pub fn bad_gateway<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::BAD_GATEWAY, message)
|
||||
}
|
||||
pub fn service_unavailable<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::SERVICE_UNAVAILABLE, message)
|
||||
}
|
||||
pub fn gateway_timeout<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::GATEWAY_TIMEOUT, message)
|
||||
}
|
||||
pub fn http_version_not_supported<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::HTTP_VERSION_NOT_SUPPORTED, message)
|
||||
}
|
||||
|
||||
// --- 2xx/3xx (rare, but for completeness) ---
|
||||
pub fn ok<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::OK, message)
|
||||
}
|
||||
pub fn created<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::CREATED, message)
|
||||
}
|
||||
pub fn accepted<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::ACCEPTED, message)
|
||||
}
|
||||
pub fn moved_permanently<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::MOVED_PERMANENTLY, message)
|
||||
}
|
||||
pub fn found<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::FOUND, message)
|
||||
}
|
||||
pub fn see_other<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::SEE_OTHER, message)
|
||||
}
|
||||
pub fn not_modified<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_MODIFIED, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HttpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.message {
|
||||
Some(msg) => write!(f, "{}: {}", self.status, msg),
|
||||
None => write!(f, "{}", self.status),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for HttpError {}
|
||||
|
||||
/// Trait to convert errors into HttpError with a given status code.
|
||||
pub trait OrHttpError<T, M>: Sized {
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError>;
|
||||
|
||||
// --- Most common user-facing status codes ---
|
||||
fn or_bad_request(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::BAD_REQUEST, message)
|
||||
}
|
||||
fn or_unauthorized(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::UNAUTHORIZED, message)
|
||||
}
|
||||
fn or_forbidden(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::FORBIDDEN, message)
|
||||
}
|
||||
fn or_not_found(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::NOT_FOUND, message)
|
||||
}
|
||||
fn or_internal_server_error(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::INTERNAL_SERVER_ERROR, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> OrHttpError<T, ()> for Result<T, E>
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.map_err(|_| HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OrHttpError<T, ()> for Option<T> {
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.ok_or_else(|| HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OrHttpError<(), ()> for bool {
|
||||
fn or_http_error(
|
||||
self,
|
||||
status: StatusCode,
|
||||
message: impl Into<String>,
|
||||
) -> Result<(), HttpError> {
|
||||
if self {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyhowMarker;
|
||||
impl<T> OrHttpError<T, AnyhowMarker> for Result<T, anyhow::Error> {
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.map_err(|_| HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for HttpError {
|
||||
fn into_response(self) -> axum_core::response::Response {
|
||||
let body = match &self.message {
|
||||
Some(msg) => msg.clone(),
|
||||
None => self
|
||||
.status
|
||||
.canonical_reason()
|
||||
.unwrap_or("Unknown error")
|
||||
.to_string(),
|
||||
};
|
||||
(self.status, body).into_response()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// #![warn(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod document;
|
||||
pub mod history;
|
||||
|
||||
mod errors;
|
||||
mod loader;
|
||||
mod server_cached;
|
||||
mod server_future;
|
||||
mod streaming;
|
||||
mod transport;
|
||||
|
||||
pub use crate::errors::*;
|
||||
pub use crate::loader::*;
|
||||
pub use crate::server_cached::*;
|
||||
pub use crate::server_future::*;
|
||||
pub use crate::streaming::*;
|
||||
pub use crate::transport::*;
|
||||
|
||||
/// Error types and utilities.
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod httperror;
|
||||
pub use httperror::*;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DioxusServerState {}
|
||||
@@ -0,0 +1,365 @@
|
||||
use dioxus_core::{
|
||||
spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, Subscribers, Task,
|
||||
};
|
||||
use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture};
|
||||
use dioxus_hooks::{use_callback, use_signal};
|
||||
use dioxus_signals::{
|
||||
read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable,
|
||||
WritableExt, WriteLock,
|
||||
};
|
||||
use futures_util::{future, pin_mut, FutureExt, StreamExt};
|
||||
use generational_box::{BorrowResult, UnsyncStorage};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::future::Future;
|
||||
use std::{cell::Cell, ops::Deref, rc::Rc};
|
||||
|
||||
/// A hook to create a resource that loads data asynchronously.
|
||||
///
|
||||
/// This hook takes a closure that returns a future. This future will be executed on both the client
|
||||
/// and the server. The loader will return `Loading` until the future resolves, at which point it will
|
||||
/// return a `Loader<T>`. If the future fails, it will return `Loading::Failed`.
|
||||
///
|
||||
/// After the loader has successfully loaded once, it will never suspend the component again, but will
|
||||
/// instead re-load the value in the background whenever any of its dependencies change.
|
||||
///
|
||||
/// If an error occurs while re-loading, `use_loader` will once again emit a `Loading::Failed` value.
|
||||
/// The `use_loader` hook will never return a suspended state after the initial load.
|
||||
///
|
||||
/// # On the server
|
||||
///
|
||||
/// On the server, this hook will block the rendering of the component (and therefore, the page) until
|
||||
/// the future resolves. Any server futures called by `use_loader` will receive the same request context
|
||||
/// as the component that called `use_loader`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
#[track_caller]
|
||||
pub fn use_loader<F, T, E>(mut future: impl FnMut() -> F + 'static) -> Result<Loader<T>, Loading>
|
||||
where
|
||||
F: Future<Output = Result<T, E>> + 'static,
|
||||
T: 'static + std::cmp::PartialEq + Serialize + DeserializeOwned,
|
||||
E: Into<dioxus_core::Error> + 'static,
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
|
||||
// todo: wire up serialize context into loader
|
||||
// let serialize_context = use_hook(crate::transport::serialize_context);
|
||||
// // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
// let storage_entry: SerializeContextEntry<T> = use_hook(|| serialize_context.create_entry());
|
||||
// use crate::{transport::SerializeContextEntry, use_server_future, use_server_future_unsuspended};
|
||||
|
||||
let mut err = use_signal(|| None as Option<CapturedError>);
|
||||
let mut value = use_signal(|| None as Option<T>);
|
||||
let mut state = use_signal(|| LoaderState::Pending);
|
||||
let (rc, changed) = use_hook(|| {
|
||||
let (rc, changed) = ReactiveContext::new_with_origin(location);
|
||||
(rc, Rc::new(Cell::new(Some(changed))))
|
||||
});
|
||||
|
||||
let callback = use_callback(move |_| {
|
||||
// Set the state to Pending when the task is restarted
|
||||
state.set(LoaderState::Pending);
|
||||
|
||||
// Create the user's task
|
||||
let fut = rc.reset_and_run_in(&mut future);
|
||||
|
||||
// Spawn a wrapper task that polls the inner future and watches its dependencies
|
||||
spawn(async move {
|
||||
// Move the future here and pin it so we can poll it
|
||||
let fut = fut;
|
||||
pin_mut!(fut);
|
||||
|
||||
// Run each poll in the context of the reactive scope
|
||||
// This ensures the scope is properly subscribed to the future's dependencies
|
||||
let res = future::poll_fn(|cx| {
|
||||
rc.run_in(|| {
|
||||
tracing::trace_span!("polling resource", location = %location)
|
||||
.in_scope(|| fut.poll_unpin(cx))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
// Map the error to the captured error type so it's cheap to clone and pass out
|
||||
// We go through the anyhow::Error first since it's more useful.
|
||||
let res: Result<T, CapturedError> = res.map_err(|e| {
|
||||
let res: dioxus_core::Error = e.into();
|
||||
res.into()
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(v) => {
|
||||
err.set(None);
|
||||
value.set(Some(v));
|
||||
state.set(LoaderState::Ready);
|
||||
}
|
||||
Err(e) => {
|
||||
err.set(Some(e));
|
||||
state.set(LoaderState::Failed);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut task = use_hook(|| Signal::new(callback(())));
|
||||
|
||||
use_hook(|| {
|
||||
let mut changed = changed.take().unwrap();
|
||||
spawn(async move {
|
||||
loop {
|
||||
// Wait for the dependencies to change
|
||||
let _ = changed.next().await;
|
||||
|
||||
// Stop the old task
|
||||
task.write().cancel();
|
||||
|
||||
// Start a new task
|
||||
task.set(callback(()));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed());
|
||||
|
||||
let handle = LoaderHandle {
|
||||
task,
|
||||
err,
|
||||
callback,
|
||||
state,
|
||||
};
|
||||
|
||||
match &*state.read_unchecked() {
|
||||
LoaderState::Pending => Err(Loading::Pending(handle)),
|
||||
LoaderState::Failed => Err(Loading::Failed(handle)),
|
||||
LoaderState::Ready => Ok(Loader {
|
||||
real_value: value,
|
||||
read_value,
|
||||
error: err,
|
||||
state,
|
||||
task,
|
||||
handle,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// A Loader is a signal that represents a value that is loaded asynchronously.
|
||||
///
|
||||
/// Once a `Loader<T>` has been successfully created from `use_loader`, it can be use like a normal signal of type `T`.
|
||||
///
|
||||
/// When the loader is re-reloading its values, it will no longer suspend its component, making it
|
||||
/// very useful for server-side-rendering.
|
||||
pub struct Loader<T> {
|
||||
/// This is a signal that unwraps the inner value. We can't give it out unless we know the inner value is Some(T)!
|
||||
read_value: ReadSignal<T>,
|
||||
|
||||
/// This is the actual signal. We let the user set this value if they want to, but we can't let them set it to `None`.
|
||||
real_value: Signal<Option<T>>,
|
||||
error: Signal<Option<CapturedError>>,
|
||||
state: Signal<LoaderState>,
|
||||
task: Signal<Task>,
|
||||
handle: LoaderHandle,
|
||||
}
|
||||
|
||||
impl<T: 'static> Loader<T> {
|
||||
/// Get the error that occurred during loading, if any.
|
||||
///
|
||||
/// After initial load, this will return `None` until the next reload fails.
|
||||
pub fn error(&self) -> Option<CapturedError> {
|
||||
self.error.read().as_ref().cloned()
|
||||
}
|
||||
|
||||
/// Restart the loading task.
|
||||
///
|
||||
/// After initial load, this won't suspend the component, but will reload in the background.
|
||||
pub fn restart(&mut self) {
|
||||
self.handle.restart();
|
||||
}
|
||||
|
||||
/// Check if the loader has failed.
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.error.read().is_some() && matches!(*self.state.read(), LoaderState::Failed)
|
||||
}
|
||||
|
||||
/// Cancel the current loading task.
|
||||
pub fn cancel(&self) {
|
||||
self.task.read().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Writable for Loader<T> {
|
||||
type WriteMetadata = <Signal<Option<T>> as Writable>::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(
|
||||
&self,
|
||||
) -> std::result::Result<
|
||||
dioxus_signals::WritableRef<'static, Self>,
|
||||
generational_box::BorrowMutError,
|
||||
>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
let writer = self.real_value.try_write_unchecked()?;
|
||||
Ok(WriteLock::map(writer, |f: &mut Option<T>| {
|
||||
f.as_mut()
|
||||
.expect("Loader value should be set if the `Loader<T>` exists")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Readable for Loader<T> {
|
||||
type Target = T;
|
||||
type Storage = UnsyncStorage;
|
||||
|
||||
#[track_caller]
|
||||
fn try_read_unchecked(
|
||||
&self,
|
||||
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Ok(self.read_value.read_unchecked())
|
||||
}
|
||||
|
||||
/// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
|
||||
///
|
||||
/// If the signal has been dropped, this will panic.
|
||||
#[track_caller]
|
||||
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Ok(self.peek_unchecked())
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> Subscribers
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.read_value.subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoAttributeValue for Loader<T>
|
||||
where
|
||||
T: Clone + IntoAttributeValue + PartialEq + 'static,
|
||||
{
|
||||
fn into_value(self) -> dioxus_core::AttributeValue {
|
||||
self.with(|f| f.clone().into_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoDynNode for Loader<T>
|
||||
where
|
||||
T: Clone + IntoDynNode + PartialEq + 'static,
|
||||
{
|
||||
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
|
||||
self().into_dyn_node()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> PartialEq for Loader<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.read_value == other.read_value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Deref for Loader<T>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
{
|
||||
type Target = dyn Fn() -> T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe { ReadableExt::deref_impl(self) }
|
||||
}
|
||||
}
|
||||
|
||||
read_impls!(Loader<T> where T: PartialEq);
|
||||
|
||||
impl<T> Clone for Loader<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for Loader<T> {}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
|
||||
pub enum LoaderState {
|
||||
/// The loader's future is still running
|
||||
Pending,
|
||||
|
||||
/// The loader's future has completed successfully
|
||||
Ready,
|
||||
|
||||
/// The loader's future has failed and now the loader is in an error state.
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct LoaderHandle {
|
||||
callback: Callback<(), Task>,
|
||||
task: Signal<Task>,
|
||||
err: Signal<Option<CapturedError>>,
|
||||
state: Signal<LoaderState>,
|
||||
}
|
||||
|
||||
impl LoaderHandle {
|
||||
/// Restart the loading task.
|
||||
pub fn restart(&mut self) {
|
||||
self.task.write().cancel();
|
||||
let new_task = self.callback.call(());
|
||||
self.task.set(new_task);
|
||||
}
|
||||
|
||||
/// Get the current state of the loader.
|
||||
pub fn state(&self) -> LoaderState {
|
||||
*self.state.read()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LoaderHandle {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for LoaderHandle {}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Loading {
|
||||
/// The loader is still pending and the component should suspend.
|
||||
Pending(LoaderHandle),
|
||||
|
||||
/// The loader has failed and an error will be returned up the tree.
|
||||
Failed(LoaderHandle),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Loading {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Loading::Pending(_) => write!(f, "Loading::Pending"),
|
||||
Loading::Failed(_) => write!(f, "Loading::Failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Loading {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Loading::Pending(_) => write!(f, "Loading is still pending"),
|
||||
Loading::Failed(_) => write!(f, "Loading has failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Loading into a RenderError for use with the `?` operator in components
|
||||
impl From<Loading> for RenderError {
|
||||
fn from(val: Loading) -> Self {
|
||||
match val {
|
||||
Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.task.cloned())),
|
||||
Loading::Failed(err) => RenderError::Error(
|
||||
err.err
|
||||
.cloned()
|
||||
.expect("LoaderHandle in Failed state should always have an error"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
-2
@@ -1,5 +1,5 @@
|
||||
use crate::transport::SerializeContextEntry;
|
||||
use dioxus_core::use_hook;
|
||||
use dioxus_fullstack_protocol::SerializeContextEntry;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// This allows you to send data from the server to the client *during hydration*.
|
||||
@@ -35,7 +35,7 @@ pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
value: impl FnOnce() -> O,
|
||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||
) -> O {
|
||||
let serialize = dioxus_fullstack_protocol::serialize_context();
|
||||
let serialize = crate::serialize_context();
|
||||
#[allow(unused)]
|
||||
let entry: SerializeContextEntry<O> = serialize.create_entry();
|
||||
#[cfg(feature = "server")]
|
||||
@@ -56,3 +56,46 @@ pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
value()
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn use_server_cached<O, M>(server_fn: impl Fn() -> O) -> O
|
||||
// where
|
||||
// O: Transportable<M> + Clone,
|
||||
// M: 'static,
|
||||
// {
|
||||
// let location = std::panic::Location::caller();
|
||||
// use_hook(|| server_cached(server_fn, location))
|
||||
// }
|
||||
|
||||
// pub(crate) fn server_cached<O, M>(
|
||||
// value: impl FnOnce() -> O,
|
||||
// #[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||
// ) -> O
|
||||
// where
|
||||
// O: Transportable<M> + Clone,
|
||||
// M: 'static,
|
||||
// {
|
||||
// let serialize = crate::transport::serialize_context();
|
||||
|
||||
// #[allow(unused)]
|
||||
// let entry: SerializeContextEntry<O> = serialize.create_entry();
|
||||
|
||||
// #[cfg(feature = "server")]
|
||||
// {
|
||||
// let data = value();
|
||||
// entry.insert(&data, location);
|
||||
// data
|
||||
// }
|
||||
|
||||
// #[cfg(all(not(feature = "server"), feature = "web"))]
|
||||
// {
|
||||
// match entry.get() {
|
||||
// Ok(value) => value,
|
||||
// Err(_) => value(),
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[cfg(not(any(feature = "server", feature = "web")))]
|
||||
// {
|
||||
// value()
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,429 @@
|
||||
use dioxus_core::{suspend, use_hook, RenderError};
|
||||
use dioxus_hooks::*;
|
||||
use dioxus_signals::ReadableExt;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::future::Future;
|
||||
|
||||
/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||
///
|
||||
///
|
||||
/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||
///
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// // ❌ The future inside of use_server_future is not reactive
|
||||
/// let id = use_signal(|| 0);
|
||||
/// use_server_future(move || {
|
||||
/// async move {
|
||||
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
/// println!("{id}");
|
||||
/// }
|
||||
/// });
|
||||
/// // ✅ The closure that creates the future for use_server_future is reactive
|
||||
/// let id = use_signal(|| 0);
|
||||
/// use_server_future(move || {
|
||||
/// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
||||
/// let cloned_id = id();
|
||||
/// async move {
|
||||
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
/// println!("{cloned_id}");
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// </div>
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn App() -> Element {
|
||||
/// let mut article_id = use_signal(|| 0);
|
||||
/// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||
/// // The future will rerun any time the
|
||||
/// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||
/// let article = use_server_future(move || fetch_article(article_id()))?;
|
||||
///
|
||||
/// rsx! {
|
||||
/// "{article().unwrap()}"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
#[track_caller]
|
||||
pub fn use_server_future<T, F>(
|
||||
mut future: impl FnMut() -> F + 'static,
|
||||
) -> Result<Resource<T>, RenderError>
|
||||
where
|
||||
T: Serialize + DeserializeOwned + 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
{
|
||||
let serialize_context = use_hook(crate::serialize_context);
|
||||
|
||||
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
#[allow(unused)]
|
||||
let storage_entry: crate::SerializeContextEntry<T> =
|
||||
use_hook(|| serialize_context.create_entry());
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
let caller = std::panic::Location::caller();
|
||||
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result =
|
||||
use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
||||
|
||||
let resource = use_resource(move || {
|
||||
#[cfg(feature = "server")]
|
||||
let storage_entry = storage_entry.clone();
|
||||
|
||||
let user_fut = future();
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result = initial_web_result.clone();
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
async move {
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
match initial_web_result.take() {
|
||||
// The data was deserialized successfully from the server
|
||||
Some(Ok(o)) => return o,
|
||||
|
||||
// The data is still pending from the server. Don't try to resolve it on the client
|
||||
Some(Err(crate::TakeDataError::DataPending)) => std::future::pending::<()>().await,
|
||||
|
||||
// The data was not available on the server, rerun the future
|
||||
Some(Err(_)) => {}
|
||||
|
||||
// This isn't the first run, so we don't need do anything
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Otherwise just run the future itself
|
||||
let out = user_fut.await;
|
||||
|
||||
// If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||
#[cfg(feature = "server")]
|
||||
storage_entry.insert(&out, caller);
|
||||
|
||||
out
|
||||
}
|
||||
});
|
||||
|
||||
// On the first run, force this task to be polled right away in case its value is ready
|
||||
use_hook(|| {
|
||||
let _ = resource.task().poll_now();
|
||||
});
|
||||
|
||||
// Suspend if the value isn't ready
|
||||
if resource.state().cloned() == UseResourceState::Pending {
|
||||
let task = resource.task();
|
||||
if !task.paused() {
|
||||
return Err(suspend(task).unwrap_err());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resource)
|
||||
}
|
||||
|
||||
// use dioxus_core::{suspend, use_hook, RenderError};
|
||||
// use dioxus_hooks::*;
|
||||
// use dioxus_signals::ReadableExt;
|
||||
// use serde::{de::DeserializeOwned, Serialize};
|
||||
// use std::future::Future;
|
||||
|
||||
// /// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||
// ///
|
||||
// ///
|
||||
// /// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||
// ///
|
||||
// ///
|
||||
// /// <div class="warning">
|
||||
// ///
|
||||
// /// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
||||
// ///
|
||||
// /// ```rust, no_run
|
||||
// /// # use dioxus::prelude::*;
|
||||
// /// // ❌ The future inside of use_server_future is not reactive
|
||||
// /// let id = use_signal(|| 0);
|
||||
// /// use_server_future(move || {
|
||||
// /// async move {
|
||||
// /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
// /// println!("{id}");
|
||||
// /// }
|
||||
// /// });
|
||||
// /// // ✅ The closure that creates the future for use_server_future is reactive
|
||||
// /// let id = use_signal(|| 0);
|
||||
// /// use_server_future(move || {
|
||||
// /// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
||||
// /// let cloned_id = id();
|
||||
// /// async move {
|
||||
// /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
// /// println!("{cloned_id}");
|
||||
// /// }
|
||||
// /// });
|
||||
// /// ```
|
||||
// ///
|
||||
// /// </div>
|
||||
// ///
|
||||
// /// # Example
|
||||
// ///
|
||||
// /// ```rust, no_run
|
||||
// /// # use dioxus::prelude::*;
|
||||
// /// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
||||
// /// use dioxus::prelude::*;
|
||||
// ///
|
||||
// /// fn App() -> Element {
|
||||
// /// let mut article_id = use_signal(|| 0);
|
||||
// /// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||
// /// // The future will rerun any time the
|
||||
// /// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||
// /// let article = use_server_future(move || fetch_article(article_id()))?;
|
||||
// ///
|
||||
// /// rsx! {
|
||||
// /// "{article().unwrap()}"
|
||||
// /// }
|
||||
// /// }
|
||||
// /// ```
|
||||
// #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
// #[track_caller]
|
||||
// pub fn use_server_future<T, F>(
|
||||
// mut future: impl FnMut() -> F + 'static,
|
||||
// ) -> Result<Resource<T>, RenderError>
|
||||
// where
|
||||
// T: Serialize + DeserializeOwned + 'static,
|
||||
// F: Future<Output = T> + 'static,
|
||||
// {
|
||||
// let serialize_context = use_hook(crate::serialize_context);
|
||||
|
||||
// // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
// #[allow(unused)]
|
||||
// let storage_entry: crate::SerializeContextEntry<T> =
|
||||
// use_hook(|| serialize_context.create_entry());
|
||||
|
||||
// #[cfg(feature = "server")]
|
||||
// let caller = std::panic::Location::caller();
|
||||
|
||||
// // If this is the first run and we are on the web client, the data might be cached
|
||||
// #[cfg(feature = "web")]
|
||||
// let initial_web_result =
|
||||
// use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
||||
|
||||
// let resource = use_resource(move || {
|
||||
// #[cfg(feature = "server")]
|
||||
// let storage_entry = storage_entry.clone();
|
||||
|
||||
// let user_fut = future();
|
||||
|
||||
// #[cfg(feature = "web")]
|
||||
// let initial_web_result = initial_web_result.clone();
|
||||
|
||||
// #[allow(clippy::let_and_return)]
|
||||
// async move {
|
||||
// // If this is the first run and we are on the web client, the data might be cached
|
||||
// #[cfg(feature = "web")]
|
||||
// match initial_web_result.take() {
|
||||
// // The data was deserialized successfully from the server
|
||||
// Some(Ok(o)) => return o,
|
||||
|
||||
// // The data is still pending from the server. Don't try to resolve it on the client
|
||||
// Some(Err(crate::TakeDataError::DataPending)) => std::future::pending::<()>().await,
|
||||
|
||||
// // The data was not available on the server, rerun the future
|
||||
// Some(Err(_)) => {}
|
||||
|
||||
// // This isn't the first run, so we don't need do anything
|
||||
// None => {}
|
||||
// }
|
||||
|
||||
// // Otherwise just run the future itself
|
||||
// let out = user_fut.await;
|
||||
|
||||
// // If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||
// #[cfg(feature = "server")]
|
||||
// storage_entry.insert(&out, caller);
|
||||
|
||||
// out
|
||||
// }
|
||||
// });
|
||||
|
||||
// // On the first run, force this task to be polled right away in case its value is ready
|
||||
// use_hook(|| {
|
||||
// let _ = resource.task().poll_now();
|
||||
// });
|
||||
|
||||
// // Suspend if the value isn't ready
|
||||
// if resource.state().cloned() == UseResourceState::Pending {
|
||||
// let task = resource.task();
|
||||
// if !task.paused() {
|
||||
// return Err(suspend(task).unwrap_err());
|
||||
// }
|
||||
// }
|
||||
|
||||
// Ok(resource)
|
||||
// }
|
||||
|
||||
// // use dioxus_core::{suspend, use_hook, RenderError};
|
||||
// // use dioxus_hooks::*;
|
||||
// // use dioxus_signals::ReadableExt;
|
||||
// // use std::future::Future;
|
||||
// // // use crate::Transportable;
|
||||
|
||||
// // /// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||
// // ///
|
||||
// // ///
|
||||
// // /// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||
// // ///
|
||||
// // ///
|
||||
// // /// <div class="warning">
|
||||
// // ///
|
||||
// // /// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
||||
// // ///
|
||||
// // /// ```rust, no_run
|
||||
// // /// # use dioxus::prelude::*;
|
||||
// // /// // ❌ The future inside of use_server_future is not reactive
|
||||
// // /// let id = use_signal(|| 0);
|
||||
// // /// use_server_future(move || {
|
||||
// // /// async move {
|
||||
// // /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
// // /// println!("{id}");
|
||||
// // /// }
|
||||
// // /// });
|
||||
// // /// // ✅ The closure that creates the future for use_server_future is reactive
|
||||
// // /// let id = use_signal(|| 0);
|
||||
// // /// use_server_future(move || {
|
||||
// // /// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
||||
// // /// let cloned_id = id();
|
||||
// // /// async move {
|
||||
// // /// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
// // /// println!("{cloned_id}");
|
||||
// // /// }
|
||||
// // /// });
|
||||
// // /// ```
|
||||
// // ///
|
||||
// // /// </div>
|
||||
// // ///
|
||||
// // /// # Example
|
||||
// // ///
|
||||
// // /// ```rust, no_run
|
||||
// // /// # use dioxus::prelude::*;
|
||||
// // /// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
||||
// // /// use dioxus::prelude::*;
|
||||
// // ///
|
||||
// // /// fn App() -> Element {
|
||||
// // /// let mut article_id = use_signal(|| 0);
|
||||
// // /// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||
// // /// // The future will rerun any time the
|
||||
// // /// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||
// // /// let article = use_server_future(move || fetch_article(article_id()))?;
|
||||
// // ///
|
||||
// // /// rsx! {
|
||||
// // /// "{article().unwrap()}"
|
||||
// // /// }
|
||||
// // /// }
|
||||
// // /// ```
|
||||
// // #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
// // #[track_caller]
|
||||
// // pub fn use_server_future<T, F, M>(
|
||||
// // future: impl FnMut() -> F + 'static,
|
||||
// // ) -> Result<Resource<T>, RenderError>
|
||||
// // where
|
||||
// // F: Future<Output = T> + 'static,
|
||||
// // T: Transportable<M>,
|
||||
// // M: 'static,
|
||||
// // {
|
||||
// // let resource = use_server_future_unsuspended(future);
|
||||
|
||||
// // // Suspend if the value isn't ready
|
||||
// // if resource.state().cloned() == UseResourceState::Pending {
|
||||
// // let task = resource.task();
|
||||
// // if !task.paused() {
|
||||
// // return Err(suspend(task).unwrap_err());
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // Ok(resource)
|
||||
// // }
|
||||
|
||||
// // /// Like [`use_server_future`] but does not suspend if the future is still running.
|
||||
// // ///
|
||||
// // /// You need to manage suspending yourself by checking the resource state.
|
||||
// // pub fn use_server_future_unsuspended<T, F, M>(
|
||||
// // mut future: impl FnMut() -> F + 'static,
|
||||
// // ) -> Resource<T>
|
||||
// // where
|
||||
// // F: Future<Output = T> + 'static,
|
||||
// // T: Transportable<M>,
|
||||
// // M: 'static,
|
||||
// // {
|
||||
// // let serialize_context = use_hook(crate::transport::serialize_context);
|
||||
|
||||
// // // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
// // #[allow(unused)]
|
||||
// // let storage_entry: crate::transport::SerializeContextEntry<T> =
|
||||
// // use_hook(|| serialize_context.create_entry());
|
||||
|
||||
// // #[cfg(feature = "server")]
|
||||
// // let caller = std::panic::Location::caller();
|
||||
|
||||
// // // If this is the first run and we are on the web client, the data might be cached
|
||||
// // #[cfg(feature = "web")]
|
||||
// // let initial_web_result =
|
||||
// // use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
||||
|
||||
// // let resource = use_resource(move || {
|
||||
// // #[cfg(feature = "server")]
|
||||
// // let storage_entry = storage_entry.clone();
|
||||
|
||||
// // let user_fut = future();
|
||||
|
||||
// // #[cfg(feature = "web")]
|
||||
// // let initial_web_result = initial_web_result.clone();
|
||||
|
||||
// // #[allow(clippy::let_and_return)]
|
||||
// // async move {
|
||||
// // // If this is the first run and we are on the web client, the data might be cached
|
||||
// // #[cfg(feature = "web")]
|
||||
// // match initial_web_result.take() {
|
||||
// // // The data was deserialized successfully from the server
|
||||
// // Some(Ok(o)) => return o,
|
||||
|
||||
// // // The data is still pending from the server. Don't try to resolve it on the client
|
||||
// // Some(Err(crate::transport::TakeDataError::DataPending)) => {
|
||||
// // std::future::pending::<()>().await
|
||||
// // }
|
||||
|
||||
// // // The data was not available on the server, rerun the future
|
||||
// // Some(Err(_)) => {}
|
||||
|
||||
// // // This isn't the first run, so we don't need do anything
|
||||
// // None => {}
|
||||
// // }
|
||||
|
||||
// // // Otherwise just run the future itself
|
||||
// // let out = user_fut.await;
|
||||
|
||||
// // // If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||
// // #[cfg(feature = "server")]
|
||||
// // storage_entry.insert(&out, caller);
|
||||
|
||||
// // out
|
||||
// // }
|
||||
// // });
|
||||
|
||||
// // // On the first run, force this task to be polled right away in case its value is ready
|
||||
// // use_hook(|| {
|
||||
// // let _ = resource.task().poll_now();
|
||||
// // });
|
||||
|
||||
// // resource
|
||||
// // }
|
||||
+4
-4
@@ -4,9 +4,9 @@ use dioxus_signals::{ReadableExt, Signal, WritableExt};
|
||||
/// The status of the streaming response
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum StreamingStatus {
|
||||
/// The initial chunk is still being rendered. The http response parts can still be modified with
|
||||
/// [DioxusServerContext::response_parts_mut](https://docs.rs/dioxus-fullstack/0.6.3/dioxus_fullstack/prelude/struct.DioxusServerContext.html#method.response_parts_mut).
|
||||
/// The initial chunk is still being rendered. The http response parts can still be modified at this point.
|
||||
RenderingInitialChunk,
|
||||
|
||||
/// The initial chunk has been committed and the response is now streaming. The http response parts
|
||||
/// have already been sent to the client and can no longer be modified.
|
||||
InitialChunkCommitted,
|
||||
@@ -59,7 +59,7 @@ impl StreamingContext {
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_fullstack_hooks::*;
|
||||
/// # use dioxus_fullstack_core::*;
|
||||
/// # fn Children() -> Element { unimplemented!() }
|
||||
/// fn App() -> Element {
|
||||
/// // This will start streaming immediately after the current render is complete.
|
||||
@@ -83,7 +83,7 @@ pub fn commit_initial_chunk() {
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_fullstack_hooks::*;
|
||||
/// # use dioxus_fullstack_core::*;
|
||||
/// #[component]
|
||||
/// fn MetaTitle(title: String) -> Element {
|
||||
/// // If streaming has already started, warn the user that the meta tag will not show
|
||||
@@ -5,6 +5,10 @@ use base64::Engine;
|
||||
use dioxus_core::CapturedError;
|
||||
use serde::Serialize;
|
||||
use std::{cell::RefCell, io::Cursor, rc::Rc};
|
||||
// use base64::Engine;
|
||||
// use dioxus_core::{CapturedError, Error};
|
||||
// use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
// use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc};
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
thread_local! {
|
||||
@@ -84,6 +88,7 @@ impl HydrationContext {
|
||||
})
|
||||
}
|
||||
|
||||
// pub(crate) fn insert<T: Transportable<M>, M: 'static>(
|
||||
pub(crate) fn insert<T: Serialize>(
|
||||
&self,
|
||||
id: usize,
|
||||
@@ -93,6 +98,7 @@ impl HydrationContext {
|
||||
self.data.borrow_mut().insert(id, value, location);
|
||||
}
|
||||
|
||||
// pub(crate) fn get<T: Transportable<M>, M: 'static>(
|
||||
pub(crate) fn get<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
id: usize,
|
||||
@@ -142,6 +148,21 @@ impl<T> SerializeContextEntry<T> {
|
||||
{
|
||||
self.context.get(self.index)
|
||||
}
|
||||
// /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
|
||||
// pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
|
||||
// where
|
||||
// T: Transportable<M>,
|
||||
// {
|
||||
// self.context.insert(self.index, value, location);
|
||||
// }
|
||||
|
||||
// /// Grab the data from the serialize context
|
||||
// pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
|
||||
// where
|
||||
// T: Transportable<M>,
|
||||
// {
|
||||
// self.context.get(self.index)
|
||||
// }
|
||||
}
|
||||
|
||||
/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
|
||||
@@ -210,6 +231,7 @@ impl Default for HTMLData {
|
||||
}
|
||||
|
||||
impl HTMLData {
|
||||
#[allow(unused)]
|
||||
fn from_serialized(
|
||||
data: &[u8],
|
||||
debug_types: Option<Vec<String>>,
|
||||
@@ -254,12 +276,14 @@ impl HTMLData {
|
||||
}
|
||||
|
||||
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||
// fn insert<T: Transportable<M>, M: 'static>(
|
||||
fn insert<T: Serialize>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
value: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
// let serialized = value.transport_to_bytes();
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(value, &mut serialized).unwrap();
|
||||
self.data[id] = Some(serialized);
|
||||
@@ -271,6 +295,7 @@ impl HTMLData {
|
||||
}
|
||||
|
||||
/// Get the data from the serialize context
|
||||
// fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
|
||||
fn get<T: serde::de::DeserializeOwned>(&self, index: usize) -> Result<T, TakeDataError> {
|
||||
if index >= self.data.len() {
|
||||
tracing::trace!(
|
||||
@@ -283,6 +308,7 @@ impl HTMLData {
|
||||
let bytes = self.data[index].as_ref();
|
||||
match bytes {
|
||||
Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
|
||||
// Some(bytes) => match T::transport_from_bytes(bytes) {
|
||||
Ok(x) => Ok(x),
|
||||
Err(err) => {
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -355,6 +381,7 @@ impl HTMLData {
|
||||
|
||||
let data = base64::engine::general_purpose::STANDARD.encode(serialized);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let format_js_list_of_strings = |list: &[Option<String>]| {
|
||||
let body = list
|
||||
.iter()
|
||||
@@ -425,3 +452,120 @@ impl std::error::Error for TakeDataError {}
|
||||
pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
|
||||
serialize_context().create_entry()
|
||||
}
|
||||
|
||||
// /// An error that can occur when trying to take data from the server
|
||||
// #[derive(thiserror::Error, Debug)]
|
||||
// pub enum TakeDataError {
|
||||
// /// Deserializing the data failed
|
||||
// #[error("DeserializationError: {0}")]
|
||||
// DeserializationError(ciborium::de::Error<std::io::Error>),
|
||||
|
||||
// /// No data was available
|
||||
// #[error("DataNotAvailable")]
|
||||
// DataNotAvailable,
|
||||
|
||||
// /// The server serialized a placeholder for the data, but it isn't available yet
|
||||
// #[error("DataPending")]
|
||||
// DataPending,
|
||||
// }
|
||||
|
||||
// /// A `Transportable` type can be safely transported from the server to the client, and be used for
|
||||
// /// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
|
||||
// /// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`.
|
||||
// ///
|
||||
// /// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
|
||||
// ///
|
||||
// /// You can also implement `Transportable` for `Result<T, dioxus_core::Error>` where `T` is
|
||||
// /// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
|
||||
// ///
|
||||
// /// Note that transporting a `Result<T, dioxus_core::Error>` will lose various aspects of the original
|
||||
// /// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message.
|
||||
// pub trait Transportable<M = ()>: 'static {
|
||||
// /// Serialize the type to a byte vector for transport
|
||||
// fn transport_to_bytes(&self) -> Vec<u8>;
|
||||
|
||||
// /// Deserialize the type from a byte slice
|
||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||
// where
|
||||
// Self: Sized;
|
||||
// }
|
||||
|
||||
// impl<T> Transportable<()> for T
|
||||
// where
|
||||
// T: Serialize + DeserializeOwned + 'static,
|
||||
// {
|
||||
// fn transport_to_bytes(&self) -> Vec<u8> {
|
||||
// let mut serialized = Vec::new();
|
||||
// ciborium::into_writer(self, &mut serialized).unwrap();
|
||||
// serialized
|
||||
// }
|
||||
|
||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||
// where
|
||||
// Self: Sized,
|
||||
// {
|
||||
// ciborium::from_reader(Cursor::new(bytes))
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// struct TransportResultErr<T> {
|
||||
// error: Result<T, CapturedError>,
|
||||
// }
|
||||
|
||||
// #[doc(hidden)]
|
||||
// pub struct TransportViaErrMarker;
|
||||
|
||||
// impl<T> Transportable<TransportViaErrMarker> for Result<T, dioxus_core::Error>
|
||||
// where
|
||||
// T: Serialize + DeserializeOwned + 'static,
|
||||
// {
|
||||
// fn transport_to_bytes(&self) -> Vec<u8> {
|
||||
// let err = TransportResultErr {
|
||||
// error: self
|
||||
// .as_ref()
|
||||
// .map_err(|e| CapturedError::from_display(e.to_string())),
|
||||
// };
|
||||
|
||||
// let mut serialized = Vec::new();
|
||||
// ciborium::into_writer(&err, &mut serialized).unwrap();
|
||||
// serialized
|
||||
// }
|
||||
|
||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||
// where
|
||||
// Self: Sized,
|
||||
// {
|
||||
// let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
|
||||
// match err.error {
|
||||
// Ok(value) => Ok(Ok(value)),
|
||||
// Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[doc(hidden)]
|
||||
// pub struct TransportCapturedError;
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// struct TransportError {
|
||||
// error: String,
|
||||
// }
|
||||
// impl Transportable<TransportCapturedError> for CapturedError {
|
||||
// fn transport_to_bytes(&self) -> Vec<u8> {
|
||||
// let err = TransportError {
|
||||
// error: self.to_string(),
|
||||
// };
|
||||
|
||||
// let mut serialized = Vec::new();
|
||||
// ciborium::into_writer(&err, &mut serialized).unwrap();
|
||||
// serialized
|
||||
// }
|
||||
|
||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||
// where
|
||||
// Self: Sized,
|
||||
// {
|
||||
// let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
|
||||
// Ok(CapturedError(Arc::new(Error::msg::<String>(err.error))))
|
||||
// }
|
||||
// }
|
||||
@@ -1,4 +0,0 @@
|
||||
mod server_cached;
|
||||
pub use server_cached::*;
|
||||
mod server_future;
|
||||
pub use server_future::*;
|
||||
@@ -1,138 +0,0 @@
|
||||
use dioxus_core::{suspend, use_hook, RenderError};
|
||||
use dioxus_hooks::*;
|
||||
use dioxus_signals::ReadableExt;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::future::Future;
|
||||
|
||||
/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||
///
|
||||
///
|
||||
/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||
///
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// // ❌ The future inside of use_server_future is not reactive
|
||||
/// let id = use_signal(|| 0);
|
||||
/// use_server_future(move || {
|
||||
/// async move {
|
||||
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
/// println!("{id}");
|
||||
/// }
|
||||
/// });
|
||||
/// // ✅ The closure that creates the future for use_server_future is reactive
|
||||
/// let id = use_signal(|| 0);
|
||||
/// use_server_future(move || {
|
||||
/// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
||||
/// let cloned_id = id();
|
||||
/// async move {
|
||||
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||
/// println!("{cloned_id}");
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// </div>
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # async fn fetch_article(id: u32) -> String { unimplemented!() }
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn App() -> Element {
|
||||
/// let mut article_id = use_signal(|| 0);
|
||||
/// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||
/// // The future will rerun any time the
|
||||
/// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||
/// let article = use_server_future(move || fetch_article(article_id()))?;
|
||||
///
|
||||
/// rsx! {
|
||||
/// "{article().unwrap()}"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||
#[track_caller]
|
||||
pub fn use_server_future<T, F>(
|
||||
mut future: impl FnMut() -> F + 'static,
|
||||
) -> Result<Resource<T>, RenderError>
|
||||
where
|
||||
T: Serialize + DeserializeOwned + 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
{
|
||||
let serialize_context = use_hook(dioxus_fullstack_protocol::serialize_context);
|
||||
|
||||
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
#[allow(unused)]
|
||||
let storage_entry: dioxus_fullstack_protocol::SerializeContextEntry<T> =
|
||||
use_hook(|| serialize_context.create_entry());
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
let caller = std::panic::Location::caller();
|
||||
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result =
|
||||
use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
|
||||
|
||||
let resource = use_resource(move || {
|
||||
#[cfg(feature = "server")]
|
||||
let storage_entry = storage_entry.clone();
|
||||
|
||||
let user_fut = future();
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
let initial_web_result = initial_web_result.clone();
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
async move {
|
||||
// If this is the first run and we are on the web client, the data might be cached
|
||||
#[cfg(feature = "web")]
|
||||
match initial_web_result.take() {
|
||||
// The data was deserialized successfully from the server
|
||||
Some(Ok(o)) => return o,
|
||||
|
||||
// The data is still pending from the server. Don't try to resolve it on the client
|
||||
Some(Err(dioxus_fullstack_protocol::TakeDataError::DataPending)) => {
|
||||
std::future::pending::<()>().await
|
||||
}
|
||||
|
||||
// The data was not available on the server, rerun the future
|
||||
Some(Err(_)) => {}
|
||||
|
||||
// This isn't the first run, so we don't need do anything
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Otherwise just run the future itself
|
||||
let out = user_fut.await;
|
||||
|
||||
// If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||
#[cfg(feature = "server")]
|
||||
storage_entry.insert(&out, caller);
|
||||
|
||||
out
|
||||
}
|
||||
});
|
||||
|
||||
// On the first run, force this task to be polled right away in case its value is ready
|
||||
use_hook(|| {
|
||||
let _ = resource.task().poll_now();
|
||||
});
|
||||
|
||||
// Suspend if the value isn't ready
|
||||
if resource.state().cloned() == UseResourceState::Pending {
|
||||
let task = resource.task();
|
||||
if !task.paused() {
|
||||
return Err(suspend(task).unwrap_err());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resource)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#![warn(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod document;
|
||||
pub mod errors;
|
||||
pub mod history;
|
||||
mod hooks;
|
||||
mod streaming;
|
||||
|
||||
pub use crate::hooks::*;
|
||||
pub use crate::streaming::*;
|
||||
@@ -1,20 +1,24 @@
|
||||
[package]
|
||||
name = "dioxus_server_macro"
|
||||
name = "dioxus-fullstack-macro"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com/docs/0.5/guide/en/getting_started/fullstack.html"
|
||||
keywords = ["dom", "ui", "gui", "react", "liveview"]
|
||||
authors = ["Jonathan Kelley", "Evan Almloff"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Server function macros for Dioxus"
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true, features = ["full"] }
|
||||
server_fn_macro = { workspace = true }
|
||||
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
], workspace = true, default-features = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
xxhash-rust = { features = ["const_xxh64" ], workspace = true, default-features = true }
|
||||
const_format = { workspace = true, default-features = true }
|
||||
convert_case = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
@@ -24,13 +28,3 @@ axum = { workspace = true }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[features]
|
||||
axum = ["server_fn_macro/axum"]
|
||||
server = ["server_fn_macro/ssr"]
|
||||
browser = []
|
||||
reqwest = ["server_fn_macro/reqwest"]
|
||||
generic = ["server_fn_macro/generic"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 424 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user