소스 검색

Merge branch 'master' into map-signal

Evan Almloff 1 년 전
부모
커밋
0c7729da05
100개의 변경된 파일2172개의 추가작업 그리고 560개의 파일을 삭제
  1. 5 0
      .cargo/config.toml
  2. 1 1
      .github/workflows/docs stable.yml
  3. 1 1
      .github/workflows/docs.yml
  4. 5 2
      .github/workflows/main.yml
  5. 1 2
      .github/workflows/miri.yml
  6. 2 2
      .github/workflows/playwright.yml
  7. 5 4
      Cargo.toml
  8. 70 4
      Makefile.toml
  9. 2 1
      README.md
  10. 1 0
      examples/calculator.rs
  11. 3 0
      examples/openid_connect_demo/.gitignore
  12. 25 0
      examples/openid_connect_demo/Cargo.toml
  13. 47 0
      examples/openid_connect_demo/Dioxus.toml
  14. 13 0
      examples/openid_connect_demo/README.md
  15. 2 0
      examples/openid_connect_demo/src/constants.rs
  16. 20 0
      examples/openid_connect_demo/src/errors.rs
  17. 60 0
      examples/openid_connect_demo/src/main.rs
  18. 1 0
      examples/openid_connect_demo/src/model/mod.rs
  19. 7 0
      examples/openid_connect_demo/src/model/user.rs
  20. 125 0
      examples/openid_connect_demo/src/oidc.rs
  21. 20 0
      examples/openid_connect_demo/src/props/client.rs
  22. 1 0
      examples/openid_connect_demo/src/props/mod.rs
  23. 17 0
      examples/openid_connect_demo/src/router.rs
  24. 38 0
      examples/openid_connect_demo/src/storage.rs
  25. 250 0
      examples/openid_connect_demo/src/views/header.rs
  26. 5 0
      examples/openid_connect_demo/src/views/home.rs
  27. 86 0
      examples/openid_connect_demo/src/views/login.rs
  28. 4 0
      examples/openid_connect_demo/src/views/mod.rs
  29. 7 0
      examples/openid_connect_demo/src/views/not_found.rs
  30. 12 0
      examples/optional_props.rs
  31. 1 0
      examples/query_segments_demo/Cargo.toml
  32. 23 1
      examples/signals.rs
  33. 1 1
      examples/tailwind/Cargo.toml
  34. 114 68
      examples/todomvc.rs
  35. 6 5
      packages/autofmt/src/buffer.rs
  36. 6 6
      packages/autofmt/src/element.rs
  37. 3 3
      packages/autofmt/src/expr.rs
  38. 108 0
      packages/autofmt/src/indent.rs
  39. 12 15
      packages/autofmt/src/lib.rs
  40. 2 2
      packages/autofmt/src/writer.rs
  41. 1 1
      packages/autofmt/tests/samples.rs
  42. 10 5
      packages/autofmt/tests/wrong.rs
  43. 0 0
      packages/autofmt/tests/wrong/comments-4sp.rsx
  44. 0 0
      packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
  45. 7 0
      packages/autofmt/tests/wrong/comments-tab.rsx
  46. 5 0
      packages/autofmt/tests/wrong/comments-tab.wrong.rsx
  47. 0 0
      packages/autofmt/tests/wrong/multi-4sp.rsx
  48. 0 0
      packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
  49. 3 0
      packages/autofmt/tests/wrong/multi-tab.rsx
  50. 5 0
      packages/autofmt/tests/wrong/multi-tab.wrong.rsx
  51. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.rsx
  52. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
  53. 8 0
      packages/autofmt/tests/wrong/multiexpr-tab.rsx
  54. 5 0
      packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
  55. 2 1
      packages/cli/Cargo.toml
  56. 5 5
      packages/cli/README.md
  57. 1 1
      packages/cli/src/assets/dioxus.toml
  58. 17 4
      packages/cli/src/builder.rs
  59. 147 76
      packages/cli/src/cli/autoformat.rs
  60. 4 4
      packages/cli/src/cli/build.rs
  61. 1 1
      packages/cli/src/config.rs
  62. 3 0
      packages/cli/src/error.rs
  63. 13 1
      packages/cli/src/logging.rs
  64. 41 49
      packages/cli/src/main.rs
  65. 21 12
      packages/cli/src/server/desktop/mod.rs
  66. 17 7
      packages/cli/src/server/mod.rs
  67. 14 11
      packages/cli/src/server/output.rs
  68. 1 0
      packages/core-macro/Cargo.toml
  69. 1 0
      packages/core-macro/src/component_body_deserializers/component.rs
  70. 292 146
      packages/core-macro/src/component_body_deserializers/inline_props.rs
  71. 1 0
      packages/core-macro/src/lib.rs
  72. 24 40
      packages/core-macro/src/props/mod.rs
  73. 129 0
      packages/core-macro/src/utils.rs
  74. 3 9
      packages/core/src/arena.rs
  75. 41 3
      packages/core/src/bump_frame.rs
  76. 1 1
      packages/core/src/diff.rs
  77. 0 2
      packages/core/src/events.rs
  78. 31 2
      packages/core/src/lazynodes.rs
  79. 3 3
      packages/core/src/lib.rs
  80. 1 1
      packages/core/src/mutations.rs
  81. 7 1
      packages/core/src/nodes.rs
  82. 2 2
      packages/core/src/scope_arena.rs
  83. 15 4
      packages/core/src/scopes.rs
  84. 1 0
      packages/desktop/Cargo.toml
  85. 9 0
      packages/desktop/build.rs
  86. 13 4
      packages/desktop/src/lib.rs
  87. 1 1
      packages/desktop/src/protocol.rs
  88. 1 1
      packages/desktop/src/webview.rs
  89. 6 6
      packages/dioxus-tui/examples/colorpicker.rs
  90. 39 7
      packages/extension/src/lib.rs
  91. 7 1
      packages/extension/src/main.ts
  92. 1 1
      packages/fermi/src/hooks/atom_root.rs
  93. 3 1
      packages/fermi/src/hooks/state.rs
  94. 0 2
      packages/fermi/src/lib.rs
  95. 1 1
      packages/fullstack/Cargo.toml
  96. 1 0
      packages/fullstack/examples/axum-hello-world/src/main.rs
  97. 62 4
      packages/fullstack/src/adapters/axum_adapter.rs
  98. 19 19
      packages/fullstack/src/adapters/mod.rs
  99. 9 2
      packages/fullstack/src/launch.rs
  100. 5 0
      packages/fullstack/src/layer.rs

+ 5 - 0
.cargo/config.toml

@@ -0,0 +1,5 @@
+# All of these variables are used in the `openid_connect_demo` example, they are set here for the CI to work, they are set here because as stated here for now: `https://doc.rust-lang.org/cargo/reference/config.html` the .cargo/config.toml of the inner workspaces are not read when being invoked from the root workspace.
+[env]
+DIOXUS_FRONT_ISSUER_URL  = ""
+DIOXUS_FRONT_CLIENT_ID = ""
+DIOXUS_FRONT_URL = ""

+ 1 - 1
.github/workflows/docs stable.yml

@@ -33,7 +33,7 @@ jobs:
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
 
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.4.3
+        uses: JamesIves/github-pages-deploy-action@v4.5.0
         with:
           branch: gh-pages # The branch the action should deploy to.
           folder: docs/nightly # The folder the action should deploy.

+ 1 - 1
.github/workflows/docs.yml

@@ -39,7 +39,7 @@ jobs:
           # cd fermi && mdbook build -d ../nightly/fermi && cd ..
 
       - name: Deploy 🚀
-        uses: JamesIves/github-pages-deploy-action@v4.4.3
+        uses: JamesIves/github-pages-deploy-action@v4.5.0
         with:
           branch: gh-pages # The branch the action should deploy to.
           folder: docs/nightly # The folder the action should deploy.

+ 5 - 2
.github/workflows/main.yml

@@ -124,8 +124,6 @@ jobs:
             }
 
     steps:
-      - uses: actions/checkout@v4
-
       - name: install stable
         uses: dtolnay/rust-toolchain@master
         with:
@@ -141,6 +139,11 @@ jobs:
           workspaces: core -> ../target
           save-if: ${{ matrix.features.key == 'all' }}
 
+      - name: Install rustfmt
+        run: rustup component add rustfmt
+
+      - uses: actions/checkout@v4
+
       - name: test
         run: |
           ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}

+ 1 - 2
.github/workflows/miri.yml

@@ -86,8 +86,7 @@ jobs:
 
         # working-directory: tokio
         env:
-          #  todo: disable memory leaks ignore
-          MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
+          MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
           PROPTEST_CASES: 10
 
       # Cache the global cargo directory, but NOT the local `target` directory which

+ 2 - 2
.github/workflows/playwright.yml

@@ -20,7 +20,7 @@ jobs:
     steps:
       # Do our best to cache the toolchain and node install steps
       - uses: actions/checkout@v4
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: 16
       - name: Install Rust
@@ -43,7 +43,7 @@ jobs:
       #     args: --path packages/cli
       - name: Run Playwright tests
         run: npx playwright test
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         if: always()
         with:
           name: playwright-report

+ 5 - 4
Cargo.toml

@@ -41,6 +41,7 @@ members = [
     "examples/tailwind",
     "examples/PWA-example",
     "examples/query_segments_demo",
+    "examples/openid_connect_demo",
     # Playwright tests
     "playwright-tests/liveview",
     "playwright-tests/web",
@@ -49,7 +50,7 @@ members = [
 exclude = ["examples/mobile_demo"]
 
 [workspace.package]
-version = "0.4.2"
+version = "0.4.3"
 
 # dependencies that are shared across packages
 [workspace.dependencies]
@@ -76,7 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
 dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
 rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
 dioxus-signals = { path = "packages/signals" }
-generational-box = { path = "packages/generational-box" }
+generational-box = { path = "packages/generational-box", version = "0.4.3" }
 dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
 dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1"  }
 dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
@@ -87,7 +88,7 @@ slab = "0.4.2"
 futures-channel = "0.3.21"
 futures-util = { version = "0.3", default-features = false }
 rustc-hash = "1.1.0"
-wasm-bindgen = "0.2.87"
+wasm-bindgen = "0.2.88"
 html_parser = "0.7.0"
 thiserror = "1.0.40"
 prettyplease = { package = "prettier-please", version = "0.2", features = [
@@ -98,7 +99,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
 # It is not meant to be published, but is used so "cargo run --example XYZ" works properly
 [package]
 name = "dioxus-examples"
-version = "0.0.0"
+version = "0.4.3"
 authors = ["Jonathan Kelley"]
 edition = "2021"
 description = "Top level crate for the Dioxus repository"

+ 70 - 4
Makefile.toml

@@ -24,12 +24,64 @@ script = [
 ]
 script_runner = "@duckscript"
 
+[tasks.format]
+command = "cargo"
+args = ["fmt", "--all"]
+
+[tasks.check]
+command = "cargo"
+args = ["check", "--workspace", "--examples", "--tests"]
+
+[tasks.clippy]
+command = "cargo"
+args = [
+  "clippy",
+  "--workspace",
+  "--examples",
+  "--tests",
+  "--",
+  "-D",
+  "warnings",
+]
+
+[tasks.tidy]
+category = "Formatting"
+dependencies = ["format", "check", "clippy"]
+description = "Format and Check workspace"
+
+[tasks.install-miri]
+toolchain = "nightly"
+install_crate = { rustup_component_name = "miri", binary = "cargo +nightly miri", test_arg = "--help" }
+private = true
+
+[tasks.miri-native]
+command = "cargo"
+toolchain = "nightly"
+dependencies = ["install-miri"]
+args = [
+  "miri",
+  "test",
+  "--package",
+  "dioxus-native-core",
+  "--test",
+  "miri_native",
+]
+
+[tasks.miri-stress]
+command = "cargo"
+toolchain = "nightly"
+dependencies = ["install-miri"]
+args = ["miri", "test", "--package", "dioxus-core", "--test", "miri_stress"]
+
+[tasks.miri]
+dependencies = ["miri-native", "miri-stress"]
+
 [tasks.tests]
 category = "Testing"
 dependencies = ["tests-setup"]
 description = "Run all tests"
-env = {CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"]}
-run_task = {name = ["test-flow", "test-with-browser"], fork = true}
+env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] }
+run_task = { name = ["test-flow", "test-with-browser"], fork = true }
 
 [tasks.build]
 command = "cargo"
@@ -42,10 +94,24 @@ private = true
 [tasks.test]
 dependencies = ["build"]
 command = "cargo"
-args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"]
+args = [
+  "test",
+  "--lib",
+  "--bins",
+  "--tests",
+  "--examples",
+  "--workspace",
+  "--exclude",
+  "dioxus-router",
+  "--exclude",
+  "dioxus-desktop",
+]
 private = true
 
 [tasks.test-with-browser]
-env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] }
+env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = [
+  "**/packages/router",
+  "**/packages/desktop",
+] }
 private = true
 workspace = true

+ 2 - 1
README.md

@@ -159,8 +159,9 @@ So... Dioxus is great, but why won't it work for me?
 
 
 ## Contributing
+- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
 - Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
-- Join the discord and ask questions!
+- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
 
 
 <a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">

+ 1 - 0
examples/calculator.rs

@@ -62,6 +62,7 @@ fn app(cx: Scope) -> Element {
         div { id: "wrapper",
             div { class: "app",
                 div { class: "calculator",
+                    tabindex: "0",
                     onkeydown: handle_key_down_event,
                     div { class: "calculator-display", val.to_string() }
                     div { class: "calculator-keypad",

+ 3 - 0
examples/openid_connect_demo/.gitignore

@@ -0,0 +1,3 @@
+/target
+/dist
+.env

+ 25 - 0
examples/openid_connect_demo/Cargo.toml

@@ -0,0 +1,25 @@
+[package]
+name = "openid_auth_demo"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+console_error_panic_hook = "0.1"
+dioxus-logger = "0.4.1"
+dioxus = { path = "../../packages/dioxus", version = "*" }
+dioxus-router = { path = "../../packages/router", version = "*" }
+dioxus-web = { path = "../../packages/web", version = "*" }
+fermi = { path = "../../packages/fermi", version = "*" }
+form_urlencoded = "1.2.0"
+gloo-storage = "0.3.0"
+log = "0.4"
+openidconnect = "3.4.0"
+reqwest = "0.11.20"
+serde = { version = "1.0.188", features = ["derive"] }
+serde_json = "1.0.105"
+thiserror = "1.0.48"
+uuid = "1.4"
+web-sys = { version = "0.3", features = ["Request", "Document"] }

+ 47 - 0
examples/openid_connect_demo/Dioxus.toml

@@ -0,0 +1,47 @@
+[application]
+
+# dioxus project name
+name = "OpenID Connect authentication demo"
+
+# default platfrom
+# you can also use `dioxus serve/build --platform XXX` to use other platform
+# value: web | desktop
+default_platform = "web"
+
+# Web `build` & `serve` dist path
+out_dir = "dist"
+
+# resource (static) file folder
+asset_dir = "public"
+
+[web.app]
+
+# HTML title tag content
+title = "OpenID Connect authentication demo"
+
+[web.watcher]
+
+index_on_404 = true
+
+watch_path = ["src"]
+
+# include `assets` in web platform
+[web.resource]
+
+# CSS style file
+style = []
+
+# Javascript code file
+script = []
+
+[web.resource.dev]
+
+# Javascript code file
+# serve: [dev-server] only
+script = []
+
+[application.plugins]
+
+available = true
+
+required = []

+ 13 - 0
examples/openid_connect_demo/README.md

@@ -0,0 +1,13 @@
+# OpenID Connect example to show how to authenticate an user
+
+The environment variables in  `.cargo/config.toml` must be set in order for this example to work(if this example is just being compiled from the root workspace, the `.cargo/config.toml` from the root workspace must be set as stated in the [Cargo book](https://doc.rust-lang.org/cargo/reference/config.html)).
+
+Once they are set, you can run `dx serve`
+
+### Environment variables summary
+
+```DIOXUS_FRONT_ISSUER_URL``` The openid-connect's issuer url 
+
+```DIOXUS_FRONT_CLIENT_ID``` The openid-connect's client id
+
+```DIOXUS_FRONT_URL``` The url the frontend is supposed to be running on, it could be for example `http://localhost:8080`

+ 2 - 0
examples/openid_connect_demo/src/constants.rs

@@ -0,0 +1,2 @@
+pub const DIOXUS_FRONT_AUTH_TOKEN: &str = "auth_token";
+pub const DIOXUS_FRONT_AUTH_REQUEST: &str = "auth_request";

+ 20 - 0
examples/openid_connect_demo/src/errors.rs

@@ -0,0 +1,20 @@
+use openidconnect::{core::CoreErrorResponseType, url, RequestTokenError, StandardErrorResponse};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error("Discovery error: {0}")]
+    OpenIdConnect(
+        #[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>,
+    ),
+    #[error("Parsing error: {0}")]
+    Parse(#[from] url::ParseError),
+    #[error("Request token error: {0}")]
+    RequestToken(
+        #[from]
+        RequestTokenError<
+            openidconnect::reqwest::Error<reqwest::Error>,
+            StandardErrorResponse<CoreErrorResponseType>,
+        >,
+    ),
+}

+ 60 - 0
examples/openid_connect_demo/src/main.rs

@@ -0,0 +1,60 @@
+#![allow(non_snake_case)]
+use dioxus::prelude::*;
+use fermi::*;
+use gloo_storage::{LocalStorage, Storage};
+use log::LevelFilter;
+pub(crate) mod constants;
+pub(crate) mod errors;
+pub(crate) mod model;
+pub(crate) mod oidc;
+pub(crate) mod props;
+pub(crate) mod router;
+pub(crate) mod storage;
+pub(crate) mod views;
+use oidc::{AuthRequestState, AuthTokenState};
+use router::Route;
+
+use dioxus_router::prelude::*;
+
+use crate::{
+    constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
+    oidc::ClientState,
+};
+pub static FERMI_CLIENT: fermi::AtomRef<ClientState> = AtomRef(|_| ClientState::default());
+
+// An option is required to prevent the component from being constantly refreshed
+pub static FERMI_AUTH_TOKEN: fermi::AtomRef<Option<AuthTokenState>> = AtomRef(|_| None);
+pub static FERMI_AUTH_REQUEST: fermi::AtomRef<Option<AuthRequestState>> = AtomRef(|_| None);
+
+pub static DIOXUS_FRONT_ISSUER_URL: &str = env!("DIOXUS_FRONT_ISSUER_URL");
+pub static DIOXUS_FRONT_CLIENT_ID: &str = env!("DIOXUS_FRONT_CLIENT_ID");
+pub static DIOXUS_FRONT_URL: &str = env!("DIOXUS_FRONT_URL");
+
+fn App(cx: Scope) -> Element {
+    use_init_atom_root(cx);
+
+    // Retrieve the value stored in the browser's storage
+    let stored_auth_token = LocalStorage::get(DIOXUS_FRONT_AUTH_TOKEN)
+        .ok()
+        .unwrap_or(AuthTokenState::default());
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    if fermi_auth_token.read().is_none() {
+        *fermi_auth_token.write() = Some(stored_auth_token);
+    }
+
+    let stored_auth_request = LocalStorage::get(DIOXUS_FRONT_AUTH_REQUEST)
+        .ok()
+        .unwrap_or(AuthRequestState::default());
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    if fermi_auth_request.read().is_none() {
+        *fermi_auth_request.write() = Some(stored_auth_request);
+    }
+    render! { Router::<Route> {} }
+}
+
+fn main() {
+    dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
+    console_error_panic_hook::set_once();
+    log::info!("starting app");
+    dioxus_web::launch(App);
+}

+ 1 - 0
examples/openid_connect_demo/src/model/mod.rs

@@ -0,0 +1 @@
+pub(crate) mod user;

+ 7 - 0
examples/openid_connect_demo/src/model/user.rs

@@ -0,0 +1,7 @@
+use uuid::Uuid;
+
+#[derive(PartialEq)]
+pub struct User {
+    pub id: Uuid,
+    pub name: String,
+}

+ 125 - 0
examples/openid_connect_demo/src/oidc.rs

@@ -0,0 +1,125 @@
+use openidconnect::{
+    core::{CoreClient, CoreErrorResponseType, CoreIdToken, CoreResponseType, CoreTokenResponse},
+    reqwest::async_http_client,
+    url::Url,
+    AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, ClientId, CsrfToken, IssuerUrl,
+    LogoutRequest, Nonce, ProviderMetadataWithLogout, RedirectUrl, RefreshToken, RequestTokenError,
+    StandardErrorResponse,
+};
+use serde::{Deserialize, Serialize};
+
+use crate::{props::client::ClientProps, DIOXUS_FRONT_CLIENT_ID};
+
+#[derive(Clone, Debug, Default)]
+pub struct ClientState {
+    pub oidc_client: Option<ClientProps>,
+}
+
+/// State that holds the nonce and authorization url and the nonce generated to log in an user
+#[derive(Clone, Deserialize, Serialize, Default)]
+pub struct AuthRequestState {
+    pub auth_request: Option<AuthRequest>,
+}
+
+#[derive(Clone, Deserialize, Serialize)]
+pub struct AuthRequest {
+    pub nonce: Nonce,
+    pub authorize_url: String,
+}
+
+/// State the tokens returned once the user is authenticated
+#[derive(Debug, Deserialize, Serialize, Default, Clone)]
+pub struct AuthTokenState {
+    /// Token used to identify the user
+    pub id_token: Option<CoreIdToken>,
+    /// Token used to refresh the tokens if they expire
+    pub refresh_token: Option<RefreshToken>,
+}
+
+pub fn email(
+    client: CoreClient,
+    id_token: CoreIdToken,
+    nonce: Nonce,
+) -> Result<String, ClaimsVerificationError> {
+    match id_token.claims(&client.id_token_verifier(), &nonce) {
+        Ok(claims) => Ok(claims.clone().email().unwrap().to_string()),
+        Err(error) => Err(error),
+    }
+}
+
+pub fn authorize_url(client: CoreClient) -> AuthRequest {
+    let (authorize_url, _csrf_state, nonce) = client
+        .authorize_url(
+            AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
+            CsrfToken::new_random,
+            Nonce::new_random,
+        )
+        .add_scope(openidconnect::Scope::new("email".to_string()))
+        .add_scope(openidconnect::Scope::new("profile".to_string()))
+        .url();
+    AuthRequest {
+        authorize_url: authorize_url.to_string(),
+        nonce,
+    }
+}
+
+pub async fn init_provider_metadata() -> Result<ProviderMetadataWithLogout, crate::errors::Error> {
+    let issuer_url = IssuerUrl::new(crate::DIOXUS_FRONT_ISSUER_URL.to_string())?;
+    Ok(ProviderMetadataWithLogout::discover_async(issuer_url, async_http_client).await?)
+}
+
+pub async fn init_oidc_client() -> Result<(ClientId, CoreClient), crate::errors::Error> {
+    let client_id = ClientId::new(crate::DIOXUS_FRONT_CLIENT_ID.to_string());
+    let provider_metadata = init_provider_metadata().await?;
+    let client_secret = None;
+    let redirect_url = RedirectUrl::new(format!("{}/login", crate::DIOXUS_FRONT_URL))?;
+
+    Ok((
+        client_id.clone(),
+        CoreClient::from_provider_metadata(provider_metadata, client_id, client_secret)
+            .set_redirect_uri(redirect_url),
+    ))
+}
+
+///TODO: Add pkce_pacifier
+pub async fn token_response(
+    oidc_client: CoreClient,
+    code: String,
+) -> Result<CoreTokenResponse, crate::errors::Error> {
+    // let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
+    Ok(oidc_client
+        .exchange_code(AuthorizationCode::new(code.clone()))
+        // .set_pkce_verifier(pkce_verifier)
+        .request_async(async_http_client)
+        .await?)
+}
+
+pub async fn exchange_refresh_token(
+    oidc_client: CoreClient,
+    refresh_token: RefreshToken,
+) -> Result<
+    CoreTokenResponse,
+    RequestTokenError<
+        openidconnect::reqwest::Error<reqwest::Error>,
+        StandardErrorResponse<CoreErrorResponseType>,
+    >,
+> {
+    oidc_client
+        .exchange_refresh_token(&refresh_token)
+        .request_async(async_http_client)
+        .await
+}
+
+pub async fn log_out_url(id_token_hint: CoreIdToken) -> Result<Url, crate::errors::Error> {
+    let provider_metadata = init_provider_metadata().await?;
+    let end_session_url = provider_metadata
+        .additional_metadata()
+        .clone()
+        .end_session_endpoint
+        .unwrap();
+    let logout_request: LogoutRequest = LogoutRequest::from(end_session_url);
+    Ok(logout_request
+        .set_client_id(ClientId::new(DIOXUS_FRONT_CLIENT_ID.to_string()))
+        .set_id_token_hint(&id_token_hint)
+        .http_get_url())
+}

+ 20 - 0
examples/openid_connect_demo/src/props/client.rs

@@ -0,0 +1,20 @@
+use dioxus::prelude::*;
+use openidconnect::{core::CoreClient, ClientId};
+
+#[derive(Props, Clone, Debug)]
+pub struct ClientProps {
+    pub client: CoreClient,
+    pub client_id: ClientId,
+}
+
+impl PartialEq for ClientProps {
+    fn eq(&self, other: &Self) -> bool {
+        self.client_id == other.client_id
+    }
+}
+
+impl ClientProps {
+    pub fn new(client_id: ClientId, client: CoreClient) -> Self {
+        ClientProps { client_id, client }
+    }
+}

+ 1 - 0
examples/openid_connect_demo/src/props/mod.rs

@@ -0,0 +1 @@
+pub(crate) mod client;

+ 17 - 0
examples/openid_connect_demo/src/router.rs

@@ -0,0 +1,17 @@
+use crate::views::{header::AuthHeader, home::Home, login::Login, not_found::NotFound};
+use dioxus::prelude::*;
+use dioxus_router::prelude::*;
+
+#[derive(Routable, Clone)]
+pub enum Route {
+    #[layout(AuthHeader)]
+    #[route("/")]
+    Home {},
+
+    // https://dioxuslabs.com/learn/0.4/router/reference/routes#query-segments
+    #[route("/login?:query_string")]
+    Login { query_string: String },
+    #[end_layout]
+    #[route("/:..route")]
+    NotFound { route: Vec<String> },
+}

+ 38 - 0
examples/openid_connect_demo/src/storage.rs

@@ -0,0 +1,38 @@
+use fermi::UseAtomRef;
+use gloo_storage::{LocalStorage, Storage};
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    constants::{DIOXUS_FRONT_AUTH_REQUEST, DIOXUS_FRONT_AUTH_TOKEN},
+    oidc::{AuthRequestState, AuthTokenState},
+};
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct StorageEntry<T> {
+    pub key: String,
+    pub value: T,
+}
+
+pub trait PersistentWrite<T: Serialize + Clone> {
+    fn persistent_set(atom_ref: &UseAtomRef<Option<T>>, entry: Option<T>);
+}
+
+impl PersistentWrite<AuthTokenState> for AuthTokenState {
+    fn persistent_set(
+        atom_ref: &UseAtomRef<Option<AuthTokenState>>,
+        entry: Option<AuthTokenState>,
+    ) {
+        *atom_ref.write() = entry.clone();
+        LocalStorage::set(DIOXUS_FRONT_AUTH_TOKEN, entry).unwrap();
+    }
+}
+
+impl PersistentWrite<AuthRequestState> for AuthRequestState {
+    fn persistent_set(
+        atom_ref: &UseAtomRef<Option<AuthRequestState>>,
+        entry: Option<AuthRequestState>,
+    ) {
+        *atom_ref.write() = entry.clone();
+        LocalStorage::set(DIOXUS_FRONT_AUTH_REQUEST, entry).unwrap();
+    }
+}

+ 250 - 0
examples/openid_connect_demo/src/views/header.rs

@@ -0,0 +1,250 @@
+use crate::{
+    oidc::{
+        authorize_url, email, exchange_refresh_token, init_oidc_client, log_out_url,
+        AuthRequestState, AuthTokenState, ClientState,
+    },
+    props::client::ClientProps,
+    router::Route,
+    storage::PersistentWrite,
+    FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
+};
+use dioxus::prelude::*;
+use dioxus_router::prelude::{Link, Outlet};
+use fermi::*;
+use openidconnect::{url::Url, OAuth2TokenResponse, TokenResponse};
+
+#[component]
+pub fn LogOut(cx: Scope<ClientProps>) -> Element {
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let fermi_auth_token_read = fermi_auth_token.read().clone();
+    let log_out_url_state = use_state(cx, || None::<Option<Result<Url, crate::errors::Error>>>);
+    cx.render(match fermi_auth_token_read {
+        Some(fermi_auth_token_read) => match fermi_auth_token_read.id_token.clone() {
+            Some(id_token) => match log_out_url_state.get() {
+                Some(log_out_url_result) => match log_out_url_result {
+                    Some(uri) => match uri {
+                        Ok(uri) => {
+                            rsx! {
+                                Link {
+                                    onclick: move |_| {
+                                        {
+                                            AuthTokenState::persistent_set(
+                                                fermi_auth_token,
+                                                Some(AuthTokenState::default()),
+                                            );
+                                        }
+                                    },
+                                    to: uri.to_string(),
+                                    "Log out"
+                                }
+                            }
+                        }
+                        Err(error) => {
+                            rsx! {
+                                div { format!{"Failed to load disconnection url: {:?}", error} }
+                            }
+                        }
+                    },
+                    None => {
+                        rsx! { div { "Loading... Please wait" } }
+                    }
+                },
+                None => {
+                    let logout_url_task = move || {
+                        cx.spawn({
+                            let log_out_url_state = log_out_url_state.to_owned();
+                            async move {
+                                let logout_url = log_out_url(id_token).await;
+                                let logout_url_option = Some(logout_url);
+                                log_out_url_state.set(Some(logout_url_option));
+                            }
+                        })
+                    };
+                    logout_url_task();
+                    rsx! { div{"Loading log out url... Please wait"}}
+                }
+            },
+            None => {
+                rsx! {{}}
+            }
+        },
+        None => {
+            rsx! {{}}
+        }
+    })
+}
+
+#[component]
+pub fn RefreshToken(cx: Scope<ClientProps>) -> Element {
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    let fermi_auth_token_read = fermi_auth_token.read().clone();
+    cx.render(match fermi_auth_token_read {
+        Some(fermi_auth_client_read) => match fermi_auth_client_read.refresh_token {
+            Some(refresh_token) => {
+                let fermi_auth_token = fermi_auth_token.to_owned();
+                let fermi_auth_request = fermi_auth_request.to_owned();
+                let client = cx.props.client.clone();
+                let exchange_refresh_token_spawn = move || {
+                    cx.spawn({
+                        async move {
+                            let exchange_refresh_token =
+                                exchange_refresh_token(client, refresh_token).await;
+                            match exchange_refresh_token {
+                                Ok(response_token) => {
+                                    AuthTokenState::persistent_set(
+                                        &fermi_auth_token,
+                                        Some(AuthTokenState {
+                                            id_token: response_token.id_token().cloned(),
+                                            refresh_token: response_token.refresh_token().cloned(),
+                                        }),
+                                    );
+                                }
+                                Err(_error) => {
+                                    AuthTokenState::persistent_set(
+                                        &fermi_auth_token,
+                                        Some(AuthTokenState::default()),
+                                    );
+                                    AuthRequestState::persistent_set(
+                                        &fermi_auth_request,
+                                        Some(AuthRequestState::default()),
+                                    );
+                                }
+                            }
+                        }
+                    })
+                };
+                exchange_refresh_token_spawn();
+                rsx! { div { "Refreshing session, please wait" } }
+            }
+            None => {
+                rsx! { div { "Id token expired and no refresh token found" } }
+            }
+        },
+        None => {
+            rsx! {{}}
+        }
+    })
+}
+
+#[component]
+pub fn LoadClient(cx: Scope) -> Element {
+    let init_client_future = use_future(cx, (), |_| async move { init_oidc_client().await });
+    let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
+    cx.render(match init_client_future.value() {
+        Some(client_props) => match client_props {
+            Ok((client_id, client)) => {
+                *fermi_client.write() = ClientState {
+                    oidc_client: Some(ClientProps::new(client_id.clone(), client.clone())),
+                };
+                rsx! {
+                    div { "Client successfully loaded" }
+                    Outlet::<Route> {}
+                }
+            }
+            Err(error) => {
+                rsx! {
+                    div { format!{"Failed to load client: {:?}", error} }
+                    log::info!{"Failed to load client: {:?}", error},
+                    Outlet::<Route> {}
+                }
+            }
+        },
+        None => {
+            rsx! {
+                div {
+                    div { "Loading client, please wait" }
+                    Outlet::<Route> {}
+                }
+            }
+        }
+    })
+}
+
+#[component]
+pub fn AuthHeader(cx: Scope) -> Element {
+    let auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    let fermi_client: &UseAtomRef<ClientState> = use_atom_ref(cx, &FERMI_CLIENT);
+    let client = fermi_client.read().oidc_client.clone();
+    let auth_request_read = fermi_auth_request.read().clone();
+    let auth_token_read = auth_token.read().clone();
+    cx.render(match (client, auth_request_read, auth_token_read) {
+        // We have everything we need to attempt to authenticate the user
+        (Some(client_props), Some(auth_request), Some(auth_token)) => {
+            match auth_request.auth_request {
+                Some(auth_request) => {
+                    match auth_token.id_token {
+                        Some(id_token) => {
+                            match email(
+                                client_props.client.clone(),
+                                id_token.clone(),
+                                auth_request.nonce.clone(),
+                            ) {
+                                Ok(email) => {
+                                    rsx! {
+                                        div {
+                                            div { email }
+                                            LogOut { client_id: client_props.client_id, client: client_props.client }
+                                            Outlet::<Route> {}
+                                        }
+                                    }
+                                }
+                                // Id token failed to be decoded
+                                Err(error) => match error {
+                                    // Id token failed to be decoded because it expired, we refresh it
+                                    openidconnect::ClaimsVerificationError::Expired(_message) => {
+                                        log::info!("Token expired");
+                                        rsx! {
+                                            div {
+                                                RefreshToken {client_id: client_props.client_id, client: client_props.client}
+                                                Outlet::<Route> {}
+                                            }
+                                        }
+                                    }
+                                    // Other issue with token decoding
+                                    _ => {
+                                        log::info!("Other issue with token");
+                                        rsx! {
+                                            div {
+                                                div { error.to_string() }
+                                                Outlet::<Route> {}
+                                            }
+                                        }
+                                    }
+                                },
+                            }
+                        }
+                        // User is not logged in
+                        None => {
+                            rsx! {
+                                div {
+                                    Link { to: auth_request.authorize_url.clone(), "Log in" }
+                                    Outlet::<Route> {}
+                                }
+                            }
+                        }
+                    }
+                }
+                None => {
+                    let auth_request = authorize_url(client_props.client);
+                    AuthRequestState::persistent_set(
+                        fermi_auth_request,
+                        Some(AuthRequestState {
+                            auth_request: Some(auth_request),
+                        }),
+                    );
+                    rsx! { div { "Loading nonce" } }
+                }
+            }
+        }
+        // Client is not initialized yet, we need it for everything
+        (None, _, _) => {
+            rsx! { LoadClient {} }
+        }
+        // We need everything loaded before doing anything
+        (_client, _auth_request, _auth_token) => {
+            rsx! {{}}
+        }
+    })
+}

+ 5 - 0
examples/openid_connect_demo/src/views/home.rs

@@ -0,0 +1,5 @@
+use dioxus::prelude::*;
+
+pub fn Home(cx: Scope) -> Element {
+    render! { div { "Hello world" } }
+}

+ 86 - 0
examples/openid_connect_demo/src/views/login.rs

@@ -0,0 +1,86 @@
+use crate::{
+    oidc::{token_response, AuthRequestState, AuthTokenState},
+    router::Route,
+    storage::PersistentWrite,
+    DIOXUS_FRONT_URL, FERMI_AUTH_REQUEST, FERMI_AUTH_TOKEN, FERMI_CLIENT,
+};
+use dioxus::prelude::*;
+use dioxus_router::prelude::{Link, NavigationTarget};
+use fermi::*;
+use openidconnect::{OAuth2TokenResponse, TokenResponse};
+
+#[component]
+pub fn Login(cx: Scope, query_string: String) -> Element {
+    let fermi_client = use_atom_ref(cx, &FERMI_CLIENT);
+    let fermi_auth_token = use_atom_ref(cx, &FERMI_AUTH_TOKEN);
+    let home_url: NavigationTarget<Route> = DIOXUS_FRONT_URL.parse().unwrap();
+    let fermi_auth_request = use_atom_ref(cx, &FERMI_AUTH_REQUEST);
+    let client = fermi_client.read().oidc_client.clone();
+    let auth_token_read = fermi_auth_token.read().clone();
+    cx.render(match (client, auth_token_read) {
+        (Some(client_props), Some(auth_token_read)) => {
+            match (auth_token_read.id_token, auth_token_read.refresh_token) {
+                (Some(_id_token), Some(_refresh_token)) => {
+                    rsx! {
+                        div { "Sign in successful" }
+                        Link { to: home_url, "Go back home" }
+                    }
+                }
+                // If the refresh token is set but not the id_token, there was an error, we just go back home and reset their value
+                (None, Some(_)) | (Some(_), None) => {
+                    rsx! {
+                        div { "Error while attempting to log in" }
+                        Link {
+                            to: home_url,
+                            onclick: move |_| {
+                                AuthTokenState::persistent_set(fermi_auth_token, Some(AuthTokenState::default()));
+                                AuthRequestState::persistent_set(
+                                    fermi_auth_request,
+                                    Some(AuthRequestState::default()),
+                                );
+                            },
+                            "Go back home"
+                        }
+                    }
+                }
+                (None, None) => {
+                    let mut query_pairs = form_urlencoded::parse(query_string.as_bytes());
+                let code_pair = query_pairs.find(|(key, _value)| key == "code");
+                match code_pair {
+                    Some((_key, code)) => {
+                        let auth_code = code.to_string();
+                        let token_response_spawn = move ||{
+                            cx.spawn({
+                                let fermi_auth_token = fermi_auth_token.to_owned();
+                                async move {
+                                    let token_response_result = token_response(client_props.client, auth_code).await;
+                                    match token_response_result{
+                                        Ok(token_response) => {
+                                            let id_token = token_response.id_token().unwrap();
+                                            AuthTokenState::persistent_set(&fermi_auth_token, Some(AuthTokenState {
+                                                id_token: Some(id_token.clone()),
+                                                refresh_token: token_response.refresh_token().cloned()
+                                            }));
+                                        }
+                                        Err(error) => {
+                                            log::warn!{"{error}"};
+                                        }
+                                    }
+                                }
+                            })
+                        };
+                        token_response_spawn();
+                        rsx!{ div {} }
+                    }
+                    None => {
+                        rsx! { div { "No code provided" } }
+                    }
+                }
+                }
+            }
+        }
+        (_, _) => {
+            rsx! {{}}
+        }
+    })
+}

+ 4 - 0
examples/openid_connect_demo/src/views/mod.rs

@@ -0,0 +1,4 @@
+pub(crate) mod header;
+pub(crate) mod home;
+pub(crate) mod login;
+pub(crate) mod not_found;

+ 7 - 0
examples/openid_connect_demo/src/views/not_found.rs

@@ -0,0 +1,7 @@
+use dioxus::prelude::*;
+
+#[component]
+pub fn NotFound(cx: Scope, route: Vec<String>) -> Element {
+    let routes = route.join("");
+    render! {rsx! {div{routes}}}
+}

+ 12 - 0
examples/optional_props.rs

@@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
             a: "asd".to_string(),
             c: "asd".to_string(),
             d: Some("asd".to_string()),
+            e: Some("asd".to_string()),
+        }
+        Button {
+            a: "asd".to_string(),
+            b: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
             e: "asd".to_string(),
         }
+        Button {
+            a: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
+        }
     })
 }
 

+ 1 - 0
examples/query_segments_demo/Cargo.toml

@@ -2,6 +2,7 @@
 name = "query_segments_demo"
 version = "0.1.0"
 edition = "2021"
+publish = false
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 

+ 23 - 1
examples/signals.rs

@@ -6,11 +6,17 @@ fn main() {
 }
 
 fn app(cx: Scope) -> Element {
+    let running = dioxus_signals::use_signal(cx, || true);
     let mut count = dioxus_signals::use_signal(cx, || 0);
+    let saved_values = dioxus_signals::use_signal(cx, || vec![0.to_string()]);
 
+    // Signals can be used in async functions without an explicit clone since they're 'static and Copy
+    // Signals are backed by a runtime that is designed to deeply integrate with Dioxus apps
     use_future!(cx, || async move {
         loop {
-            count += 1;
+            if running.value() {
+                count += 1;
+            }
             tokio::time::sleep(Duration::from_millis(400)).await;
         }
     });
@@ -19,9 +25,25 @@ fn app(cx: Scope) -> Element {
         h1 { "High-Five counter: {count}" }
         button { onclick: move |_| count += 1, "Up high!" }
         button { onclick: move |_| count -= 1, "Down low!" }
+        button { onclick: move |_| running.toggle(), "Toggle counter" }
+        button { onclick: move |_| saved_values.push(count.value().to_string()), "Save this value" }
+        button { onclick: move |_| saved_values.write().clear(), "Clear saved values" }
 
+        // We can do boolean operations on the current signal value
         if count.value() > 5 {
             rsx!{ h2 { "High five!" } }
         }
+
+        // We can cleanly map signals with iterators
+        for value in saved_values.read().iter() {
+            h3 { "Saved value: {value}" }
+        }
+
+        // We can also use the signal value as a slice
+        if let [ref first, .., ref last] = saved_values.read().as_slice() {
+            rsx! { li { "First and last: {first}, {last}" } }
+        } else {
+            rsx! { "No saved values" }
+        }
     })
 }

+ 1 - 1
examples/tailwind/Cargo.toml

@@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
 dioxus-desktop = { path = "../../packages/desktop" }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
-dioxus-web = { path = "../../packages/web" }
+dioxus-web = { path = "../../packages/web" }

+ 114 - 68
examples/todomvc.rs

@@ -24,8 +24,6 @@ pub struct TodoItem {
 pub fn app(cx: Scope<()>) -> Element {
     let todos = use_state(cx, im_rc::HashMap::<u32, TodoItem>::default);
     let filter = use_state(cx, || FilterState::All);
-    let draft = use_state(cx, || "".to_string());
-    let todo_id = use_state(cx, || 0);
 
     // Filter the todos based on the filter state
     let mut filtered_todos = todos
@@ -47,42 +45,11 @@ pub fn app(cx: Scope<()>) -> Element {
 
     let show_clear_completed = todos.values().any(|todo| todo.checked);
 
-    let selected = |state| {
-        if *filter == state {
-            "selected"
-        } else {
-            "false"
-        }
-    };
-
     cx.render(rsx! {
         section { class: "todoapp",
             style { include_str!("./assets/todomvc.css") }
-            header { class: "header",
-                h1 {"todos"}
-                input {
-                    class: "new-todo",
-                    placeholder: "What needs to be done?",
-                    value: "{draft}",
-                    autofocus: "true",
-                    oninput: move |evt| {
-                        draft.set(evt.value.clone());
-                    },
-                    onkeydown: move |evt| {
-                        if evt.key() == Key::Enter && !draft.is_empty() {
-                            todos.make_mut().insert(
-                                **todo_id,
-                                TodoItem {
-                                    id: **todo_id,
-                                    checked: false,
-                                    contents: draft.to_string(),
-                                },
-                            );
-                            *todo_id.make_mut() += 1;
-                            draft.set("".to_string());
-                        }
-                    }
-                }
+            TodoHeader {
+                todos: todos,
             }
             section {
                 class: "main",
@@ -111,44 +78,56 @@ pub fn app(cx: Scope<()>) -> Element {
                     }))
                 }
                 (!todos.is_empty()).then(|| rsx!(
-                    footer { class: "footer",
-                        span { class: "todo-count",
-                            strong {"{active_todo_count} "}
-                            span {"{active_todo_text} left"}
-                        }
-                        ul { class: "filters",
-                            for (state, state_text, url) in [
-                                (FilterState::All, "All", "#/"),
-                                (FilterState::Active, "Active", "#/active"),
-                                (FilterState::Completed, "Completed", "#/completed"),
-                            ] {
-                                li {
-                                    a {
-                                        href: url,
-                                        class: selected(state),
-                                        onclick: move |_| filter.set(state),
-                                        prevent_default: "onclick",
-                                        state_text
-                                    }
-                                }
-                            }
-                        }
-                        show_clear_completed.then(|| rsx!(
-                            button {
-                                class: "clear-completed",
-                                onclick: move |_| todos.make_mut().retain(|_, todo| !todo.checked),
-                                "Clear completed"
-                            }
-                        ))
+                    ListFooter {
+                        active_todo_count: active_todo_count,
+                        active_todo_text: active_todo_text,
+                        show_clear_completed: show_clear_completed,
+                        todos: todos,
+                        filter: filter,
                     }
                 ))
             }
         }
-        footer { class: "info",
-            p { "Double-click to edit a todo" }
-            p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
-            p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
+        PageFooter {}
+    })
+}
+
+#[derive(Props)]
+pub struct TodoHeaderProps<'a> {
+    todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
+}
+
+pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
+    let draft = use_state(cx, || "".to_string());
+    let todo_id = use_state(cx, || 0);
+
+    cx.render(rsx! {
+        header { class: "header",
+        h1 {"todos"}
+        input {
+            class: "new-todo",
+            placeholder: "What needs to be done?",
+            value: "{draft}",
+            autofocus: "true",
+            oninput: move |evt| {
+                draft.set(evt.value.clone());
+            },
+            onkeydown: move |evt| {
+                if evt.key() == Key::Enter && !draft.is_empty() {
+                    cx.props.todos.make_mut().insert(
+                        **todo_id,
+                        TodoItem {
+                            id: **todo_id,
+                            checked: false,
+                            contents: draft.to_string(),
+                        },
+                    );
+                    *todo_id.make_mut() += 1;
+                    draft.set("".to_string());
+                }
+            }
         }
+    }
     })
 }
 
@@ -209,3 +188,70 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
         }
     })
 }
+
+#[derive(Props)]
+pub struct ListFooterProps<'a> {
+    todos: &'a UseState<im_rc::HashMap<u32, TodoItem>>,
+    active_todo_count: usize,
+    active_todo_text: &'a str,
+    show_clear_completed: bool,
+    filter: &'a UseState<FilterState>,
+}
+
+pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
+    let active_todo_count = cx.props.active_todo_count;
+    let active_todo_text = cx.props.active_todo_text;
+
+    let selected = |state| {
+        if *cx.props.filter == state {
+            "selected"
+        } else {
+            "false"
+        }
+    };
+
+    cx.render(rsx! {
+        footer { class: "footer",
+            span { class: "todo-count",
+                strong {"{active_todo_count} "}
+                span {"{active_todo_text} left"}
+            }
+            ul { class: "filters",
+                for (state, state_text, url) in [
+                    (FilterState::All, "All", "#/"),
+                    (FilterState::Active, "Active", "#/active"),
+                    (FilterState::Completed, "Completed", "#/completed"),
+                ] {
+                    li {
+                        a {
+                            href: url,
+                            class: selected(state),
+                            onclick: move |_| cx.props.filter.set(state),
+                            prevent_default: "onclick",
+                            state_text
+                        }
+                    }
+                }
+            }
+            if cx.props.show_clear_completed {
+                cx.render(rsx! {
+                    button {
+                        class: "clear-completed",
+                        onclick: move |_| cx.props.todos.make_mut().retain(|_, todo| !todo.checked),
+                        "Clear completed"
+                    }
+                })
+            }
+        }
+    })
+}
+
+pub fn PageFooter(cx: Scope) -> Element {
+    cx.render(rsx! {
+        footer { class: "info",
+            p { "Double-click to edit a todo" }
+            p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
+            p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
+        }
+    })
+}

+ 6 - 5
packages/autofmt/src/buffer.rs

@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
 
 use dioxus_rsx::IfmtInput;
 
-use crate::write_ifmt;
+use crate::{indent::IndentOptions, write_ifmt};
 
 /// The output buffer that tracks indent and string
 #[derive(Debug, Default)]
 pub struct Buffer {
     pub buf: String,
-    pub indent: usize,
+    pub indent_level: usize,
+    pub indent: IndentOptions,
 }
 
 impl Buffer {
@@ -31,16 +32,16 @@ impl Buffer {
     }
 
     pub fn tab(&mut self) -> Result {
-        self.write_tabs(self.indent)
+        self.write_tabs(self.indent_level)
     }
 
     pub fn indented_tab(&mut self) -> Result {
-        self.write_tabs(self.indent + 1)
+        self.write_tabs(self.indent_level + 1)
     }
 
     pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
         for _ in 0..num {
-            write!(self.buf, "    ")?
+            write!(self.buf, "{}", self.indent.indent_str())?
         }
         Ok(())
     }

+ 6 - 6
packages/autofmt/src/element.rs

@@ -66,7 +66,7 @@ impl Writer<'_> {
 
         // check if we have a lot of attributes
         let attr_len = self.is_short_attrs(attributes);
-        let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
+        let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
         let children_len = self.is_short_children(children);
         let is_small_children = children_len.is_some();
 
@@ -86,7 +86,7 @@ impl Writer<'_> {
 
         // if we have few children and few attributes, make it a one-liner
         if is_short_attr_list && is_small_children {
-            if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
+            if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
                 opt_level = ShortOptimization::Oneliner;
             } else {
                 opt_level = ShortOptimization::PropsOnTop;
@@ -185,11 +185,11 @@ impl Writer<'_> {
         }
 
         while let Some(attr) = attr_iter.next() {
-            self.out.indent += 1;
+            self.out.indent_level += 1;
             if !sameline {
                 self.write_comments(attr.attr.start())?;
             }
-            self.out.indent -= 1;
+            self.out.indent_level -= 1;
 
             if !sameline {
                 self.out.indented_tabbed_line()?;
@@ -398,14 +398,14 @@ impl Writer<'_> {
         for idx in start.line..end.line {
             let line = &self.src[idx];
             if line.trim().starts_with("//") {
-                for _ in 0..self.out.indent + 1 {
+                for _ in 0..self.out.indent_level + 1 {
                     write!(self.out, "    ")?
                 }
                 writeln!(self.out, "{}", line.trim()).unwrap();
             }
         }
 
-        for _ in 0..self.out.indent {
+        for _ in 0..self.out.indent_level {
             write!(self.out, "    ")?
         }
 

+ 3 - 3
packages/autofmt/src/expr.rs

@@ -29,7 +29,7 @@ impl Writer<'_> {
         let first_line = &self.src[start.line - 1];
         write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
 
-        let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
+        let prev_block_indent_level = self.out.indent.count_indents(first_line);
 
         for (id, line) in self.src[start.line..end.line].iter().enumerate() {
             writeln!(self.out)?;
@@ -43,9 +43,9 @@ impl Writer<'_> {
             };
 
             // trim the leading whitespace
-            let previous_indent = crate::leading_whitespaces(line) / 4;
+            let previous_indent = self.out.indent.count_indents(line);
             let offset = previous_indent.saturating_sub(prev_block_indent_level);
-            let required_indent = self.out.indent + offset;
+            let required_indent = self.out.indent_level + offset;
             self.out.write_tabs(required_indent)?;
 
             let line = line.trim_start();

+ 108 - 0
packages/autofmt/src/indent.rs

@@ -0,0 +1,108 @@
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum IndentType {
+    Spaces,
+    Tabs,
+}
+
+#[derive(Debug, Clone)]
+pub struct IndentOptions {
+    width: usize,
+    indent_string: String,
+}
+
+impl IndentOptions {
+    pub fn new(typ: IndentType, width: usize) -> Self {
+        assert_ne!(width, 0, "Cannot have an indent width of 0");
+        Self {
+            width,
+            indent_string: match typ {
+                IndentType::Tabs => "\t".into(),
+                IndentType::Spaces => " ".repeat(width),
+            },
+        }
+    }
+
+    /// Gets a string containing one indent worth of whitespace
+    pub fn indent_str(&self) -> &str {
+        &self.indent_string
+    }
+
+    /// Computes the line length in characters, counting tabs as the indent width.
+    pub fn line_length(&self, line: &str) -> usize {
+        line.chars()
+            .map(|ch| if ch == '\t' { self.width } else { 1 })
+            .sum()
+    }
+
+    /// Estimates how many times the line has been indented.
+    pub fn count_indents(&self, mut line: &str) -> usize {
+        let mut indent = 0;
+        while !line.is_empty() {
+            // Try to count tabs
+            let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
+            if num_tabs > 0 {
+                indent += num_tabs;
+                line = &line[num_tabs..];
+                continue;
+            }
+
+            // Try to count spaces
+            let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
+            if num_spaces >= self.width {
+                // Intentionally floor here to take only the amount of space that matches an indent
+                let num_space_indents = num_spaces / self.width;
+                indent += num_space_indents;
+                line = &line[num_space_indents * self.width..];
+                continue;
+            }
+
+            // Line starts with either non-indent characters or an unevent amount of spaces,
+            // so no more indent remains.
+            break;
+        }
+        indent
+    }
+}
+
+impl Default for IndentOptions {
+    fn default() -> Self {
+        Self::new(IndentType::Spaces, 4)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn count_indents() {
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
+            0
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("    v += 2"),
+            1
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("        v += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("          v += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t  v += 2"),
+            2
+        );
+        assert_eq!(
+            IndentOptions::new(IndentType::Spaces, 2).count_indents("    v += 2"),
+            2
+        );
+    }
+}

+ 12 - 15
packages/autofmt/src/lib.rs

@@ -16,8 +16,11 @@ mod collect_macros;
 mod component;
 mod element;
 mod expr;
+mod indent;
 mod writer;
 
+pub use indent::{IndentOptions, IndentType};
+
 /// A modification to the original file to be applied by an IDE
 ///
 /// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
@@ -47,7 +50,7 @@ pub struct FormattedBlock {
 /// back to the file precisely.
 ///
 /// Nested blocks of RSX will be handled automatically
-pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
+pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
     let mut formatted_blocks = Vec::new();
 
     let parsed = syn::parse_file(contents).unwrap();
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
     }
 
     let mut writer = Writer::new(contents);
+    writer.out.indent = indent;
 
     // Don't parse nested macros
     let mut end_span = LineColumn { column: 0, line: 0 };
@@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
 
         let rsx_start = macro_path.span().start();
 
-        writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
+        writer.out.indent_level = writer
+            .out
+            .indent
+            .count_indents(writer.src[rsx_start.line - 1]);
 
         write_body(&mut writer, &body);
 
@@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
     buf.consume()
 }
 
-pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
+pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
     let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
 
     let mut buf = Writer::new(block);
 
-    buf.out.indent = indent_level;
+    buf.out.indent = indent;
+    buf.out.indent_level = indent_level;
 
     write_body(&mut buf, &body);
 
@@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
     let display = DisplayIfmt(input);
     write!(writable, "{}", display)
 }
-
-pub fn leading_whitespaces(input: &str) -> usize {
-    input
-        .chars()
-        .map_while(|c| match c {
-            ' ' => Some(1),
-            '\t' => Some(4),
-            _ => None,
-        })
-        .sum()
-}

+ 2 - 2
packages/autofmt/src/writer.rs

@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
 
     // Push out the indent level and write each component, line by line
     pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
-        self.out.indent += 1;
+        self.out.indent_level += 1;
 
         self.write_body_no_indent(children)?;
 
-        self.out.indent -= 1;
+        self.out.indent_level -= 1;
         Ok(())
     }
 

+ 1 - 1
packages/autofmt/tests/samples.rs

@@ -12,7 +12,7 @@ macro_rules! twoway {
             #[test]
             fn $name() {
                 let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
-                let formatted = dioxus_autofmt::fmt_file(src);
+                let formatted = dioxus_autofmt::fmt_file(src, Default::default());
                 let out = dioxus_autofmt::apply_formats(src, formatted);
                 // normalize line endings
                 let out = out.replace("\r", "");

+ 10 - 5
packages/autofmt/tests/wrong.rs

@@ -1,10 +1,12 @@
+use dioxus_autofmt::{IndentOptions, IndentType};
+
 macro_rules! twoway {
-    ($val:literal => $name:ident) => {
+    ($val:literal => $name:ident ($indent:expr)) => {
         #[test]
         fn $name() {
             let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
             let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
-            let formatted = dioxus_autofmt::fmt_file(src_wrong);
+            let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
             let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
 
             // normalize line endings
@@ -16,8 +18,11 @@ macro_rules! twoway {
     };
 }
 
-twoway!("comments" => comments);
+twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
 
-twoway!("multi" => multi);
+twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
 
-twoway!("multiexpr" => multiexpr);
+twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
+twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));

+ 0 - 0
packages/autofmt/tests/wrong/comments.rsx → packages/autofmt/tests/wrong/comments-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/comments.wrong.rsx → packages/autofmt/tests/wrong/comments-4sp.wrong.rsx


+ 7 - 0
packages/autofmt/tests/wrong/comments-tab.rsx

@@ -0,0 +1,7 @@
+rsx! {
+	div {
+		// Comments
+		class: "asdasd",
+		"hello world"
+	}
+}

+ 5 - 0
packages/autofmt/tests/wrong/comments-tab.wrong.rsx

@@ -0,0 +1,5 @@
+rsx! {
+	div {
+		// Comments
+		class: "asdasd", "hello world" }
+}

+ 0 - 0
packages/autofmt/tests/wrong/multi.rsx → packages/autofmt/tests/wrong/multi-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/multi.wrong.rsx → packages/autofmt/tests/wrong/multi-4sp.wrong.rsx


+ 3 - 0
packages/autofmt/tests/wrong/multi-tab.rsx

@@ -0,0 +1,3 @@
+fn app(cx: Scope) -> Element {
+	cx.render(rsx! { div { "hello world" } })
+}

+ 5 - 0
packages/autofmt/tests/wrong/multi-tab.wrong.rsx

@@ -0,0 +1,5 @@
+fn app(cx: Scope) -> Element {
+	cx.render(rsx! {
+		div {"hello world" }
+	})
+}

+ 0 - 0
packages/autofmt/tests/wrong/multiexpr.rsx → packages/autofmt/tests/wrong/multiexpr-4sp.rsx


+ 0 - 0
packages/autofmt/tests/wrong/multiexpr.wrong.rsx → packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx


+ 8 - 0
packages/autofmt/tests/wrong/multiexpr-tab.rsx

@@ -0,0 +1,8 @@
+fn ItWroks() {
+	cx.render(rsx! {
+		div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
+			left,
+			right
+		}
+	})
+}

+ 5 - 0
packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx

@@ -0,0 +1,5 @@
+fn ItWroks() {
+	cx.render(rsx! {
+		div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
+	})
+}

+ 2 - 1
packages/cli/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "dioxus-cli"
-version = "0.4.1"
+version = "0.4.3"
 authors = ["Jonathan Kelley"]
 edition = "2021"
 description = "CLI tool for developing, testing, and publishing Dioxus apps"
@@ -83,6 +83,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
 dioxus-core = { workspace = true, features = ["serialize"] }
 dioxus-hot-reload = { workspace = true }
 interprocess-docfix = { version = "1.2.2" }
+gitignore = "1.0.8"
 
 [features]
 default = []

+ 5 - 5
packages/cli/README.md

@@ -10,8 +10,8 @@ It handles building, bundling, development and publishing to simplify developmen
 
 ### Install the stable version (recommended)
 
-```
-cargo install dioxus-cli --locked
+```shell
+cargo install dioxus-cli
 ```
 
 ### Install the latest development build through git
@@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
 However, this is not fully tested.
 That means you're probably going to have more bugs despite having the latest bug fixes.
 
-```
+```shell
 cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
 ```
 
@@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
 
 ### Install from local folder
 
-```
+```shell
 cargo install --path . --debug
 ```
 
@@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
 
 Alternatively, you can specify the template path:
 
-```
+```shell
 dx create hello --template gh:dioxuslabs/dioxus-template
 ```
 

+ 1 - 1
packages/cli/src/assets/dioxus.toml

@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
 
 index_on_404 = true
 
-watch_path = ["src"]
+watch_path = ["src", "examples"]
 
 # include `assets` in web platform
 [web.resource]

+ 17 - 4
packages/cli/src/builder.rs

@@ -48,14 +48,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
 
     // [1] Build the .wasm module
     log::info!("🚅 Running build command...");
+
+    let wasm_check_command = std::process::Command::new("rustup")
+        .args(["show"])
+        .output()?;
+    let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
+    if !wasm_check_output.contains("wasm32-unknown-unknown") {
+        log::info!("wasm32-unknown-unknown target not detected, installing..");
+        let _ = std::process::Command::new("rustup")
+            .args(["target", "add", "wasm32-unknown-unknown"])
+            .output()?;
+    }
+
     let cmd = subprocess::Exec::cmd("cargo");
     let cmd = cmd
         .cwd(crate_dir)
         .arg("build")
         .arg("--target")
         .arg("wasm32-unknown-unknown")
-        .arg("--message-format=json")
-        .arg("--quiet");
+        .arg("--message-format=json");
 
     let cmd = if config.release {
         cmd.arg("--release")
@@ -65,7 +76,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
     let cmd = if config.verbose {
         cmd.arg("--verbose")
     } else {
-        cmd
+        cmd.arg("--quiet")
     };
 
     let cmd = if config.custom_profile.is_some() {
@@ -261,6 +272,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
     }
     if config.verbose {
         cmd = cmd.arg("--verbose");
+    } else {
+        cmd = cmd.arg("--quiet");
     }
 
     if config.custom_profile.is_some() {
@@ -468,7 +481,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
         .unwrap_or_default()
         .contains_key("tailwindcss")
     {
-        style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
+        style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
     }
 
     replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);

+ 147 - 76
packages/cli/src/cli/autoformat.rs

@@ -1,3 +1,4 @@
+use dioxus_autofmt::{IndentOptions, IndentType};
 use futures::{stream::FuturesUnordered, StreamExt};
 use std::{fs, path::Path, process::exit};
 
@@ -26,16 +27,19 @@ pub struct Autoformat {
 impl Autoformat {
     // Todo: autoformat the entire crate
     pub async fn autoformat(self) -> Result<()> {
+        let Autoformat { check, raw, file } = self;
+
         // Default to formatting the project
-        if self.raw.is_none() && self.file.is_none() {
-            if let Err(e) = autoformat_project(self.check).await {
+        if raw.is_none() && file.is_none() {
+            if let Err(e) = autoformat_project(check).await {
                 eprintln!("error formatting project: {}", e);
                 exit(1);
             }
         }
 
-        if let Some(raw) = self.raw {
-            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
+        if let Some(raw) = raw {
+            let indent = indentation_for(".")?;
+            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
                 println!("{}", inner);
             } else {
                 // exit process with error
@@ -45,43 +49,90 @@ impl Autoformat {
         }
 
         // Format single file
-        if let Some(file) = self.file {
-            let file_content = if file == "-" {
-                let mut contents = String::new();
-                std::io::stdin().read_to_string(&mut contents)?;
-                Ok(contents)
-            } else {
-                fs::read_to_string(&file)
-            };
-
-            match file_content {
-                Ok(s) => {
-                    let edits = dioxus_autofmt::fmt_file(&s);
-                    let out = dioxus_autofmt::apply_formats(&s, edits);
-                    if file == "-" {
-                        print!("{}", out);
-                    } else {
-                        match fs::write(&file, out) {
-                            Ok(_) => {
-                                println!("formatted {}", file);
-                            }
-                            Err(e) => {
-                                eprintln!("failed to write formatted content to file: {}", e);
-                            }
-                        }
-                    }
-                }
-                Err(e) => {
-                    eprintln!("failed to open file: {}", e);
-                    exit(1);
-                }
-            }
+        if let Some(file) = file {
+            refactor_file(file)?;
         }
 
         Ok(())
     }
 }
 
+fn refactor_file(file: String) -> Result<(), Error> {
+    let indent = indentation_for(".")?;
+    let file_content = if file == "-" {
+        let mut contents = String::new();
+        std::io::stdin().read_to_string(&mut contents)?;
+        Ok(contents)
+    } else {
+        fs::read_to_string(&file)
+    };
+    let Ok(s) = file_content else {
+        eprintln!("failed to open file: {}", file_content.unwrap_err());
+        exit(1);
+    };
+    let edits = dioxus_autofmt::fmt_file(&s, indent);
+    let out = dioxus_autofmt::apply_formats(&s, edits);
+
+    if file == "-" {
+        print!("{}", out);
+    } else if let Err(e) = fs::write(&file, out) {
+        eprintln!("failed to write formatted content to file: {e}",);
+    } else {
+        println!("formatted {}", file);
+    }
+
+    Ok(())
+}
+
+fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
+    let mut files = vec![];
+
+    let gitignore_path = config.crate_dir.join(".gitignore");
+    if gitignore_path.is_file() {
+        let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
+        if let Ok(git_files) = gitigno.included_files() {
+            let git_files = git_files
+                .into_iter()
+                .filter(|f| f.ends_with(".rs") && !is_target_dir(f));
+            files.extend(git_files)
+        };
+    } else {
+        collect_rs_files(&config.crate_dir, &mut files);
+    }
+
+    files
+}
+
+fn is_target_dir(file: &Path) -> bool {
+    let stripped = if let Ok(cwd) = std::env::current_dir() {
+        file.strip_prefix(cwd).unwrap_or(file)
+    } else {
+        file
+    };
+    if let Some(first) = stripped.components().next() {
+        first.as_os_str() == "target"
+    } else {
+        false
+    }
+}
+
+async fn format_file(
+    path: impl AsRef<Path>,
+    indent: IndentOptions,
+) -> Result<usize, tokio::io::Error> {
+    let contents = tokio::fs::read_to_string(&path).await?;
+
+    let edits = dioxus_autofmt::fmt_file(&contents, indent);
+    let len = edits.len();
+
+    if !edits.is_empty() {
+        let out = dioxus_autofmt::apply_formats(&contents, edits);
+        tokio::fs::write(path, out).await?;
+    }
+
+    Ok(len)
+}
+
 /// Read every .rs file accessible when considering the .gitignore and try to format it
 ///
 /// Runs using Tokio for multithreading, so it should be really really fast
@@ -90,42 +141,27 @@ impl Autoformat {
 async fn autoformat_project(check: bool) -> Result<()> {
     let crate_config = crate::CrateConfig::new(None)?;
 
-    let mut files_to_format = vec![];
-    collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
+    let files_to_format = get_project_files(&crate_config);
+
+    if files_to_format.is_empty() {
+        return Ok(());
+    }
+
+    let indent = indentation_for(&files_to_format[0])?;
 
     let counts = files_to_format
         .into_iter()
-        .filter(|file| {
-            if file.components().any(|f| f.as_os_str() == "target") {
-                return false;
-            }
-
-            true
-        })
         .map(|path| async {
-            let _path = path.clone();
-            let res = tokio::spawn(async move {
-                let contents = tokio::fs::read_to_string(&path).await?;
-
-                let edits = dioxus_autofmt::fmt_file(&contents);
-                let len = edits.len();
-
-                if !edits.is_empty() {
-                    let out = dioxus_autofmt::apply_formats(&contents, edits);
-                    tokio::fs::write(&path, out).await?;
-                }
-
-                Ok(len) as Result<usize, tokio::io::Error>
-            })
-            .await;
+            let path_clone = path.clone();
+            let res = tokio::spawn(format_file(path, indent.clone())).await;
 
             match res {
                 Err(err) => {
-                    eprintln!("error formatting file: {}\n{err}", _path.display());
+                    eprintln!("error formatting file: {}\n{err}", path_clone.display());
                     None
                 }
                 Ok(Err(err)) => {
-                    eprintln!("error formatting file: {}\n{err}", _path.display());
+                    eprintln!("error formatting file: {}\n{err}", path_clone.display());
                     None
                 }
                 Ok(Ok(res)) => Some(res),
@@ -135,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
         .collect::<Vec<_>>()
         .await;
 
-    let files_formatted: usize = counts
-        .into_iter()
-        .map(|f| match f {
-            Some(res) => res,
-            _ => 0,
-        })
-        .sum();
+    let files_formatted: usize = counts.into_iter().flatten().sum();
 
     if files_formatted > 0 && check {
         eprintln!("{} files needed formatting", files_formatted);
@@ -151,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> {
     Ok(())
 }
 
-fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
-    let Ok(folder) = folder.read_dir() else {
+fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
+    let out = std::process::Command::new("cargo")
+        .args(["fmt", "--", "--print-config", "current"])
+        .arg(file_or_dir.as_ref())
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::inherit())
+        .output()?;
+    if !out.status.success() {
+        return Err(Error::CargoError("cargo fmt failed".into()));
+    }
+
+    let config = String::from_utf8_lossy(&out.stdout);
+
+    let hard_tabs = config
+        .lines()
+        .find(|line| line.starts_with("hard_tabs "))
+        .and_then(|line| line.split_once('='))
+        .map(|(_, value)| value.trim() == "true")
+        .ok_or_else(|| {
+            Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
+        })?;
+    let tab_spaces = config
+        .lines()
+        .find(|line| line.starts_with("tab_spaces "))
+        .and_then(|line| line.split_once('='))
+        .map(|(_, value)| value.trim().parse::<usize>())
+        .ok_or_else(|| {
+            Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
+        })?
+        .map_err(|_| {
+            Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
+        })?;
+
+    Ok(IndentOptions::new(
+        if hard_tabs {
+            IndentType::Tabs
+        } else {
+            IndentType::Spaces
+        },
+        tab_spaces,
+    ))
+}
+
+fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
+    if is_target_dir(folder.as_ref()) {
+        return;
+    }
+    let Ok(folder) = folder.as_ref().read_dir() else {
         return;
     };
-
     // load the gitignore
-
     for entry in folder {
         let Ok(entry) = entry else {
             continue;
         };
-
         let path = entry.path();
-
         if path.is_dir() {
             collect_rs_files(&path, files);
         }
-
         if let Some(ext) = path.extension() {
-            if ext == "rs" {
+            if ext == "rs" && !is_target_dir(&path) {
                 files.push(path);
             }
         }

+ 4 - 4
packages/cli/src/cli/build.rs

@@ -37,8 +37,8 @@ impl Build {
             .platform
             .unwrap_or(crate_config.dioxus_config.application.default_platform);
 
-        #[cfg(feature = "plugin")]
-        let _ = PluginManager::on_build_start(&crate_config, &platform);
+        // #[cfg(feature = "plugin")]
+        // let _ = PluginManager::on_build_start(&crate_config, &platform);
 
         match platform {
             Platform::Web => {
@@ -66,8 +66,8 @@ impl Build {
         )?;
         file.write_all(temp.as_bytes())?;
 
-        #[cfg(feature = "plugin")]
-        let _ = PluginManager::on_build_finish(&crate_config, &platform);
+        // #[cfg(feature = "plugin")]
+        // let _ = PluginManager::on_build_finish(&crate_config, &platform);
 
         Ok(())
     }

+ 1 - 1
packages/cli/src/config.rs

@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
                 },
                 proxy: Some(vec![]),
                 watcher: WebWatcherConfig {
-                    watch_path: Some(vec![PathBuf::from("src")]),
+                    watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
                     reload_html: Some(false),
                     index_on_404: Some(true),
                 },

+ 3 - 0
packages/cli/src/error.rs

@@ -29,6 +29,9 @@ pub enum Error {
     #[error("Cargo Error: {0}")]
     CargoError(String),
 
+    #[error("Couldn't retrieve cargo metadata")]
+    CargoMetadata(#[source] cargo_metadata::Error),
+
     #[error("{0}")]
     CustomError(String),
 

+ 13 - 1
packages/cli/src/logging.rs

@@ -28,7 +28,19 @@ pub fn set_up_logging() {
                 message = message,
             ));
         })
-        .level(log::LevelFilter::Info)
+        .level(match std::env::var("DIOXUS_LOG") {
+            Ok(level) => match level.to_lowercase().as_str() {
+                "error" => log::LevelFilter::Error,
+                "warn" => log::LevelFilter::Warn,
+                "info" => log::LevelFilter::Info,
+                "debug" => log::LevelFilter::Debug,
+                "trace" => log::LevelFilter::Trace,
+                _ => {
+                    panic!("Invalid log level: {}", level)
+                }
+            },
+            Err(_) => log::LevelFilter::Info,
+        })
         .chain(std::io::stdout())
         .apply()
         .unwrap();

+ 41 - 49
packages/cli/src/main.rs

@@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
 
 use Commands::*;
 
-fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
-    const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
-
-    if let Some(ref bin) = bin {
-        let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
-            .map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
-
-        if let Some(workspace) = manifest.workspace {
-            for item in workspace.members.iter() {
-                let path = PathBuf::from(item);
-
-                if !path.exists() {
-                    continue;
-                }
-
-                if !path.is_dir() {
-                    continue;
-                }
-
-                if path.ends_with(bin.clone()) {
-                    return Ok(Some(path));
-                }
-            }
-        } else {
-            return Err(Error::CargoError(ERR_MESSAGE.to_string()));
-        }
-    }
-
-    // If the bin exists but we couldn't find it
-    if bin.is_some() {
-        return Err(Error::CargoError(
-            "The specified bin does not exist.".to_string(),
-        ));
-    }
-
-    Ok(None)
+fn get_bin(bin: Option<String>) -> Result<PathBuf> {
+    let metadata = cargo_metadata::MetadataCommand::new()
+        .exec()
+        .map_err(Error::CargoMetadata)?;
+    let package = if let Some(bin) = bin {
+        metadata
+            .workspace_packages()
+            .into_iter()
+            .find(|p| p.name == bin)
+            .ok_or(format!("no such package: {}", bin))
+            .map_err(Error::CargoError)?
+    } else {
+        metadata
+            .root_package()
+            .ok_or("no root package?".into())
+            .map_err(Error::CargoError)?
+    };
+
+    let crate_dir = package
+        .manifest_path
+        .parent()
+        .ok_or("couldn't take parent dir".into())
+        .map_err(Error::CargoError)?;
+
+    Ok(crate_dir.into())
 }
 
 #[tokio::main]
@@ -53,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
 
     set_up_logging();
 
-    let bin = get_bin(args.bin)?;
+    let bin = get_bin(args.bin);
 
-    let _dioxus_config = DioxusConfig::load(bin.clone())
+    if let Ok(bin) = &bin {
+        let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
         .map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
         .unwrap_or_else(|| {
             log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
             DioxusConfig::default()
         });
 
-    #[cfg(feature = "plugin")]
-    PluginManager::init(_dioxus_config.plugin)
-        .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
+        #[cfg(feature = "plugin")]
+        PluginManager::init(_dioxus_config.plugin)
+            .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
+    }
 
     match args.action {
         Translate(opts) => opts
             .translate()
             .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
 
-        Build(opts) => opts
-            .build(bin.clone())
+        Build(opts) if bin.is_ok() => opts
+            .build(Some(bin.unwrap().clone()))
             .map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
 
-        Clean(opts) => opts
-            .clean(bin.clone())
+        Clean(opts) if bin.is_ok() => opts
+            .clean(Some(bin.unwrap().clone()))
             .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
 
-        Serve(opts) => opts
-            .serve(bin.clone())
+        Serve(opts) if bin.is_ok() => opts
+            .serve(Some(bin.unwrap().clone()))
             .await
             .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
 
@@ -92,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
             .config()
             .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
 
-        Bundle(opts) => opts
-            .bundle(bin.clone())
+        Bundle(opts) if bin.is_ok() => opts
+            .bundle(Some(bin.unwrap().clone()))
             .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
 
         #[cfg(feature = "plugin")]
@@ -118,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
 
             Ok(())
         }
+        _ => Err(anyhow::anyhow!(bin.unwrap_err())),
     }
 }

+ 21 - 12
packages/cli/src/server/desktop/mod.rs

@@ -43,8 +43,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
 
             let hot_reload_tx = broadcast::channel(100).0;
 
-            clear_paths();
-
             Some(HotReloadState {
                 messages: hot_reload_tx.clone(),
                 file_map: file_map.clone(),
@@ -73,6 +71,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
 
             move || {
                 let mut current_child = currently_running_child.write().unwrap();
+                log::trace!("Killing old process");
                 current_child.kill()?;
                 let (child, result) = start_desktop(&config)?;
                 *current_child = child;
@@ -109,7 +108,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
 }
 
 async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
-    match LocalSocketListener::bind("@dioxusin") {
+    let metadata = cargo_metadata::MetadataCommand::new()
+        .no_deps()
+        .exec()
+        .unwrap();
+    let target_dir = metadata.target_directory.as_std_path();
+    let path = target_dir.join("dioxusin");
+    clear_paths(&path);
+    match LocalSocketListener::bind(path) {
         Ok(local_socket_stream) => {
             let aborted = Arc::new(Mutex::new(false));
             // States
@@ -121,9 +127,9 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
                 let file_map = hot_reload_state.file_map.clone();
                 let channels = channels.clone();
                 let aborted = aborted.clone();
-                let _ = local_socket_stream.set_nonblocking(true);
                 move || {
                     loop {
+                        //accept() will block the thread when local_socket_stream is in blocking mode (default)
                         match local_socket_stream.accept() {
                             Ok(mut connection) => {
                                 // send any templates than have changed before the socket connected
@@ -148,7 +154,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
                                 println!("Connected to hot reloading 🚀");
                             }
                             Err(err) => {
-                                if err.kind() != std::io::ErrorKind::WouldBlock {
+                                let error_string = err.to_string();
+                                // Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
+                                let display_error = err.kind() != std::io::ErrorKind::WouldBlock
+                                    && !error_string.contains("Waiting for a process");
+                                if display_error {
                                     println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
                                 }
                             }
@@ -181,17 +191,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
     Ok(())
 }
 
-fn clear_paths() {
+fn clear_paths(file_socket_path: &std::path::Path) {
     if cfg!(target_os = "macos") {
         // On unix, if you force quit the application, it can leave the file socket open
         // This will cause the local socket listener to fail to open
         // We check if the file socket is already open from an old session and then delete it
-        let paths = ["./dioxusin", "./@dioxusin"];
-        for path in paths {
-            let path = std::path::PathBuf::from(path);
-            if path.exists() {
-                let _ = std::fs::remove_file(path);
-            }
+
+        if file_socket_path.exists() {
+            let _ = std::fs::remove_file(file_socket_path);
         }
     }
 }
@@ -212,6 +219,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
 
 pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
     // Run the desktop application
+    log::trace!("Building application");
     let result = crate::builder::build_desktop(config, true)?;
 
     match &config.executable {
@@ -222,6 +230,7 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
             if cfg!(windows) {
                 file.set_extension("exe");
             }
+            log::trace!("Running application from {:?}", file);
             let child = Command::new(file.to_str().unwrap()).spawn()?;
 
             Ok((child, result))

+ 17 - 7
packages/cli/src/server/mod.rs

@@ -32,7 +32,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
         .watcher
         .watch_path
         .clone()
-        .unwrap_or_else(|| vec![PathBuf::from("src")]);
+        .unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
 
     let watcher_config = config.clone();
     let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
@@ -55,6 +55,16 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
                             break;
                         }
 
+                        // Workaround for notify and vscode-like editor:
+                        // when edit & save a file in vscode, there will be two notifications,
+                        // the first one is a file with empty content.
+                        // filter the empty file notification to avoid false rebuild during hot-reload
+                        if let Ok(metadata) = fs::metadata(path) {
+                            if metadata.len() == 0 {
+                                continue;
+                            }
+                        }
+
                         match rsx_file_map.update_rsx(path, &config.crate_dir) {
                             Ok(UpdateResult::UpdatedRsx(msgs)) => {
                                 messages.extend(msgs);
@@ -121,12 +131,12 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
     .unwrap();
 
     for sub_path in allow_watch_path {
-        watcher
-            .watch(
-                &config.crate_dir.join(sub_path),
-                notify::RecursiveMode::Recursive,
-            )
-            .unwrap();
+        if let Err(err) = watcher.watch(
+            &config.crate_dir.join(sub_path),
+            notify::RecursiveMode::Recursive,
+        ) {
+            log::error!("Failed to watch path: {}", err);
+        }
     }
     Ok(watcher)
 }

+ 14 - 11
packages/cli/src/server/output.rs

@@ -22,17 +22,20 @@ pub fn print_console_info(
     options: PrettierOptions,
     web_info: Option<WebServerInfo>,
 ) {
-    if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
-        "cls"
-    } else {
-        "clear"
-    })
-    .output()
-    {
-        print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
-    } else {
-        // Try ANSI-Escape characters
-        print!("\x1b[2J\x1b[H");
+    // Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
+    if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
+        if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
+            "cls"
+        } else {
+            "clear"
+        })
+        .output()
+        {
+            print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
+        } else {
+            // Try ANSI-Escape characters
+            print!("\x1b[2J\x1b[H");
+        }
     }
 
     let mut profile = if config.release { "Release" } else { "Debug" }.to_string();

+ 1 - 0
packages/core-macro/Cargo.toml

@@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] }
 dioxus-rsx = { workspace = true }
 dioxus-core = { workspace = true }
 constcat = "0.3.0"
+prettyplease = "0.2.15"
 
 # testing
 [dev-dependencies]

+ 1 - 0
packages/core-macro/src/component_body_deserializers/component.rs

@@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
         block: parse_quote! {
             {
                 #[warn(non_snake_case)]
+                #[allow(clippy::inline_always)]
                 #[inline(always)]
                 #inner_comp_fn
                 #inner_comp_ident (#cx_pat)

+ 292 - 146
packages/core-macro/src/component_body_deserializers/inline_props.rs

@@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput {
 impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
     fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
         Ok(InlinePropsDeserializerOutput {
-            comp_fn: Self::get_function(component_body),
-            props_struct: Self::get_props_struct(component_body),
+            comp_fn: get_function(component_body),
+            props_struct: get_props_struct(component_body),
         })
     }
 }
 
-impl InlinePropsDeserializerArgs {
-    fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
-        let ComponentBody { item_fn, .. } = component_body;
-        let ItemFn { vis, sig, .. } = item_fn;
-        let Signature {
-            inputs,
-            ident: fn_ident,
-            generics,
-            ..
-        } = sig;
-
-        // Skip first arg since that's the context
-        let struct_fields = inputs.iter().skip(1).map(move |f| {
-            match f {
-                FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
-                FnArg::Typed(pt) => {
-                    let arg_pat = &pt.pat; // Pattern (identifier)
-                    let arg_colon = &pt.colon_token;
-                    let arg_ty = &pt.ty; // Type
-                    let arg_attrs = &pt.attrs; // Attributes
-
-                    quote! {
-                        #(#arg_attrs)
-                        *
-                        #vis #arg_pat #arg_colon #arg_ty
-                    }
+fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
+    let ComponentBody { item_fn, .. } = component_body;
+    let ItemFn { vis, sig, .. } = item_fn;
+    let Signature {
+        inputs,
+        ident: fn_ident,
+        generics,
+        ..
+    } = sig;
+
+    // Skip first arg since that's the context
+    let struct_fields = inputs.iter().skip(1).map(move |f| {
+        match f {
+            FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
+            FnArg::Typed(pt) => {
+                let arg_pat = &pt.pat; // Pattern (identifier)
+                let arg_colon = &pt.colon_token;
+                let arg_ty = &pt.ty; // Type
+                let arg_attrs = &pt.attrs; // Attributes
+
+                quote! {
+                    #(#arg_attrs)
+                    *
+                    #vis #arg_pat #arg_colon #arg_ty
                 }
             }
-        });
+        }
+    });
 
-        let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
-        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
-            Some(lt)
-        } else {
-            None
-        };
-
-        let struct_attrs = if first_lifetime.is_some() {
-            quote! { #[derive(Props)] }
-        } else {
-            quote! { #[derive(Props, PartialEq)] }
-        };
-
-        let struct_generics = if first_lifetime.is_some() {
-            let struct_generics: Punctuated<GenericParam, Comma> = component_body
-                .item_fn
-                .sig
-                .generics
-                .params
-                .iter()
-                .map(|it| match it {
-                    GenericParam::Type(tp) => {
-                        let mut tp = tp.clone();
-                        tp.bounds.push(parse_quote!( 'a ));
-
-                        GenericParam::Type(tp)
-                    }
-                    _ => it.clone(),
-                })
-                .collect();
-
-            quote! { <#struct_generics> }
-        } else {
-            quote! { #generics }
-        };
-
-        parse_quote! {
-            #struct_attrs
-            #[allow(non_camel_case_types)]
-            #vis struct #struct_ident #struct_generics
-            {
-                #(#struct_fields),*
-            }
+    let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+    let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+        Some(lt)
+    } else {
+        None
+    };
+
+    let struct_attrs = if first_lifetime.is_some() {
+        quote! { #[derive(Props)] }
+    } else {
+        quote! { #[derive(Props, PartialEq)] }
+    };
+
+    let struct_generics = if first_lifetime.is_some() {
+        let struct_generics: Punctuated<GenericParam, Comma> = component_body
+            .item_fn
+            .sig
+            .generics
+            .params
+            .iter()
+            .map(|it| match it {
+                GenericParam::Type(tp) => {
+                    let mut tp = tp.clone();
+                    tp.bounds.push(parse_quote!( 'a ));
+
+                    GenericParam::Type(tp)
+                }
+                _ => it.clone(),
+            })
+            .collect();
+
+        quote! { <#struct_generics> }
+    } else {
+        quote! { #generics }
+    };
+
+    parse_quote! {
+        #struct_attrs
+        #[allow(non_camel_case_types)]
+        #vis struct #struct_ident #struct_generics
+        {
+            #(#struct_fields),*
         }
     }
+}
+
+fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
+    if inputs.len() <= 1 {
+        return Vec::new();
+    }
 
-    fn get_function(component_body: &ComponentBody) -> ItemFn {
-        let ComponentBody {
-            item_fn,
-            cx_pat_type,
-            ..
-        } = component_body;
-        let ItemFn {
-            attrs: fn_attrs,
-            vis,
-            sig,
-            block: fn_block,
-        } = item_fn;
-        let Signature {
-            inputs,
-            ident: fn_ident,
-            generics,
-            output: fn_output,
-            asyncness,
-            ..
-        } = sig;
-        let Generics { where_clause, .. } = generics;
-
-        let cx_pat = &cx_pat_type.pat;
-        let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
-        // Skip first arg since that's the context
-        let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+    let arg_docs = inputs
+        .iter()
+        .filter_map(|f| match f {
             FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
-            FnArg::Typed(t) => Some(&t.pat),
+            FnArg::Typed(pt) => {
+                let arg_doc = pt
+                    .attrs
+                    .iter()
+                    .filter_map(|attr| {
+                        // TODO: Error reporting
+                        // Check if the path of the attribute is "doc"
+                        if !is_attr_doc(attr) {
+                            return None;
+                        };
+
+                        let Meta::NameValue(meta_name_value) = &attr.meta else {
+                            return None;
+                        };
+
+                        let Expr::Lit(doc_lit) = &meta_name_value.value else {
+                            return None;
+                        };
+
+                        let Lit::Str(doc_lit_str) = &doc_lit.lit else {
+                            return None;
+                        };
+
+                        Some(doc_lit_str.value())
+                    })
+                    .fold(String::new(), |mut doc, next_doc_line| {
+                        doc.push('\n');
+                        doc.push_str(&next_doc_line);
+                        doc
+                    });
+
+                Some((
+                    &pt.pat,
+                    &pt.ty,
+                    pt.attrs.iter().find_map(|attr| {
+                        if attr.path() != &parse_quote!(deprecated) {
+                            return None;
+                        }
+
+                        let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
+
+                        match res {
+                            Err(e) => panic!("{}", e.to_string()),
+                            Ok(v) => Some(v),
+                        }
+                    }),
+                    arg_doc,
+                ))
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let mut props_docs = Vec::with_capacity(5);
+    let props_def_link = fn_ident.to_string() + "Props";
+    let header =
+        format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
+
+    props_docs.push(parse_quote! {
+        #[doc = #header]
+    });
+
+    for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
+        let arg_name = arg_name.into_token_stream().to_string();
+        let arg_type = crate::utils::format_type_string(arg_type);
+
+        let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
+            .replace("\n\n", "</p><p>");
+        let prop_def_link = format!("{props_def_link}::{arg_name}");
+        let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
+
+        if let Some(deprecation) = deprecation {
+            arg_doc.push_str("<p>👎 Deprecated");
+
+            if let Some(since) = deprecation.since {
+                arg_doc.push_str(&format!(" since {since}"));
+            }
+
+            if let Some(note) = deprecation.note {
+                let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
+                let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
+
+                arg_doc.push_str(&format!(": {note}"));
+            }
+
+            arg_doc.push_str("</p>");
+
+            if !input_arg_doc.is_empty() {
+                arg_doc.push_str("<hr/>");
+            }
+        }
+
+        if !input_arg_doc.is_empty() {
+            arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
+        }
+
+        props_docs.push(parse_quote! {
+            #[doc = #arg_doc]
         });
+    }
+
+    props_docs
+}
+
+fn get_function(component_body: &ComponentBody) -> ItemFn {
+    let ComponentBody {
+        item_fn,
+        cx_pat_type,
+        ..
+    } = component_body;
+    let ItemFn {
+        attrs: fn_attrs,
+        vis,
+        sig,
+        block: fn_block,
+    } = item_fn;
+    let Signature {
+        inputs,
+        ident: fn_ident,
+        generics,
+        output: fn_output,
+        asyncness,
+        ..
+    } = sig;
+    let Generics { where_clause, .. } = generics;
+
+    let cx_pat = &cx_pat_type.pat;
+    let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+    // Skip first arg since that's the context
+    let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+        FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
+        FnArg::Typed(pt) => Some(&pt.pat),
+    });
+
+    let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+        Some(lt)
+    } else {
+        None
+    };
+
+    let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
+        (quote! { #lt, }, generics.clone())
+    } else {
+        let lifetime: LifetimeParam = parse_quote! { 'a };
+
+        let mut fn_generics = generics.clone();
+        fn_generics
+            .params
+            .insert(0, GenericParam::Lifetime(lifetime.clone()));
 
-        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
-            Some(lt)
-        } else {
-            None
-        };
-
-        let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
-            (quote! { #lt, }, generics.clone())
-        } else {
-            let lifetime: LifetimeParam = parse_quote! { 'a };
-
-            let mut fn_generics = generics.clone();
-            fn_generics
-                .params
-                .insert(0, GenericParam::Lifetime(lifetime.clone()));
-
-            (quote! { #lifetime, }, fn_generics)
-        };
-
-        let generics_no_bounds = {
-            let mut generics = generics.clone();
-            generics.params = generics
-                .params
-                .iter()
-                .map(|it| match it {
-                    GenericParam::Type(tp) => {
-                        let mut tp = tp.clone();
-                        tp.bounds.clear();
-
-                        GenericParam::Type(tp)
-                    }
-                    _ => it.clone(),
-                })
-                .collect();
-
-            generics
-        };
-
-        parse_quote! {
-            #(#fn_attrs)*
-            #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
-            #where_clause
-            {
-                let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
-                #fn_block
+        (quote! { #lifetime, }, fn_generics)
+    };
+
+    let generics_no_bounds = {
+        let mut generics = generics.clone();
+        generics.params = generics
+            .params
+            .iter()
+            .map(|it| match it {
+                GenericParam::Type(tp) => {
+                    let mut tp = tp.clone();
+                    tp.bounds.clear();
+
+                    GenericParam::Type(tp)
+                }
+                _ => it.clone(),
+            })
+            .collect();
+
+        generics
+    };
+
+    let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
+
+    parse_quote! {
+        #(#fn_attrs)*
+        #(#props_docs)*
+        #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
+        #where_clause
+        {
+            let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
+            #fn_block
+        }
+    }
+}
+
+/// Checks if the attribute is a `#[doc]` attribute.
+fn is_attr_doc(attr: &Attribute) -> bool {
+    attr.path() == &parse_quote!(doc)
+}
+
+fn keep_up_to_n_consecutive_chars(
+    input: &str,
+    n_of_consecutive_chars_allowed: usize,
+    target_char: char,
+) -> String {
+    let mut output = String::new();
+    let mut prev_char: Option<char> = None;
+    let mut consecutive_count = 0;
+
+    for c in input.chars() {
+        match prev_char {
+            Some(prev) if c == target_char && prev == target_char => {
+                if consecutive_count < n_of_consecutive_chars_allowed {
+                    output.push(c);
+                    consecutive_count += 1;
+                }
+            }
+            _ => {
+                output.push(c);
+                prev_char = Some(c);
+                consecutive_count = 1;
             }
         }
     }
+
+    output
 }

+ 1 - 0
packages/core-macro/src/lib.rs

@@ -12,6 +12,7 @@ use syn::{parse_macro_input, Path, Token};
 mod component_body;
 mod component_body_deserializers;
 mod props;
+mod utils;
 
 // mod rsx;
 use crate::component_body::ComponentBody;

+ 24 - 40
packages/core-macro/src/props/mod.rs

@@ -243,10 +243,6 @@ mod field_info {
             }
             .into()
         }
-
-        pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
-            type_from_inside_option(self.ty, check_option_name)
-        }
     }
 
     #[derive(Debug, Default, Clone)]
@@ -551,18 +547,16 @@ mod struct_info {
             let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
                 args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
             });
-            let phantom_generics = self.generics.params.iter().map(|param| match param {
+            let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
                 syn::GenericParam::Lifetime(lifetime) => {
                     let lifetime = &lifetime.lifetime;
-                    quote!(::core::marker::PhantomData<&#lifetime ()>)
+                    Some(quote!(::core::marker::PhantomData<&#lifetime ()>))
                 }
                 syn::GenericParam::Type(ty) => {
                     let ty = &ty.ident;
-                    quote!(::core::marker::PhantomData<#ty>)
-                }
-                syn::GenericParam::Const(_cnst) => {
-                    quote!()
+                    Some(quote!(::core::marker::PhantomData<#ty>))
                 }
+                syn::GenericParam::Const(_cnst) => None,
             });
             let builder_method_doc = match self.builder_attr.builder_method_doc {
                 Some(ref doc) => quote!(#doc),
@@ -633,7 +627,7 @@ Finally, call `.build()` to create the instance of `{name}`.
             Ok(quote! {
                 impl #impl_generics #name #ty_generics #where_clause {
                     #[doc = #builder_method_doc]
-                    #[allow(dead_code)]
+                    #[allow(dead_code, clippy::type_complexity)]
                     #vis fn builder() -> #builder_name #generics_with_empty {
                         #builder_name {
                             fields: #empties_tuple,
@@ -701,6 +695,14 @@ Finally, call `.build()` to create the instance of `{name}`.
         }
 
         pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
+            let FieldInfo {
+                name: field_name,
+                ty: field_type,
+                ..
+            } = field;
+            if *field_name == "key" {
+                return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
+            }
             let StructInfo {
                 ref builder_name, ..
             } = *self;
@@ -715,11 +717,6 @@ Finally, call `.build()` to create the instance of `{name}`.
             });
             let reconstructing = self.included_fields().map(|f| f.name);
 
-            let FieldInfo {
-                name: field_name,
-                ty: field_type,
-                ..
-            } = field;
             let mut ty_generics: Vec<syn::GenericArgument> = self
                 .generics
                 .params
@@ -782,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
                 None => quote!(),
             };
 
-            // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
-            // nesting is different so we have to do this little dance.
-            let arg_type = if field.builder_attr.strip_option {
-                field.type_from_inside_option(false).ok_or_else(|| {
-                    Error::new_spanned(
-                        field_type,
-                        "can't `strip_option` - field is not `Option<...>`",
+            let arg_type = field_type;
+            let (arg_type, arg_expr) =
+                if field.builder_attr.auto_into || field.builder_attr.strip_option {
+                    (
+                        quote!(impl ::core::convert::Into<#arg_type>),
+                        quote!(#field_name.into()),
                     )
-                })?
-            } else {
-                field_type
-            };
-            let (arg_type, arg_expr) = if field.builder_attr.auto_into {
-                (
-                    quote!(impl ::core::convert::Into<#arg_type>),
-                    quote!(#field_name.into()),
-                )
-            } else {
-                (quote!(#arg_type), quote!(#field_name))
-            };
-            let arg_expr = if field.builder_attr.strip_option {
-                quote!(Some(#arg_expr))
-            } else {
-                arg_expr
-            };
+                } else {
+                    (quote!(#arg_type), quote!(#field_name))
+                };
 
             let repeated_fields_error_type_name = syn::Ident::new(
                 &format!(
@@ -822,6 +804,7 @@ Finally, call `.build()` to create the instance of `{name}`.
                 #[allow(dead_code, non_camel_case_types, missing_docs)]
                 impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
                     #doc
+                    #[allow(clippy::type_complexity)]
                     pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
                         let #field_name = (#arg_expr,);
                         let ( #(#descructuring,)* ) = self.fields;
@@ -840,6 +823,7 @@ Finally, call `.build()` to create the instance of `{name}`.
                     #[deprecated(
                         note = #repeated_fields_error_message
                     )]
+                    #[allow(clippy::type_complexity)]
                     pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
                         self
                     }

+ 129 - 0
packages/core-macro/src/utils.rs

@@ -0,0 +1,129 @@
+use quote::ToTokens;
+use syn::parse::{Parse, ParseStream};
+use syn::spanned::Spanned;
+use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
+
+const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
+const FORMATTED_TYPE_END: &str = "= todo!();";
+
+/// Attempts to convert the given literal to a string.
+/// Converts ints and floats to their base 10 counterparts.
+///
+/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
+/// and the byte string could not be converted to UTF-8.
+pub fn lit_to_string(lit: Lit) -> Option<String> {
+    match lit {
+        Lit::Str(l) => Some(l.value()),
+        Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
+        Lit::Byte(l) => Some(String::from(l.value() as char)),
+        Lit::Char(l) => Some(l.value().to_string()),
+        Lit::Int(l) => Some(l.base10_digits().to_string()),
+        Lit::Float(l) => Some(l.base10_digits().to_string()),
+        Lit::Bool(l) => Some(l.value().to_string()),
+        Lit::Verbatim(_) => None,
+        _ => None,
+    }
+}
+
+pub fn format_type_string(ty: &Type) -> String {
+    let ty_unformatted = ty.into_token_stream().to_string();
+    let ty_unformatted = ty_unformatted.trim();
+
+    // This should always be valid syntax.
+    // Not Rust code, but syntax, which is the only thing that `syn` cares about.
+    let Ok(file_unformatted) = syn::parse_file(&format!(
+        "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
+    )) else {
+        return ty_unformatted.to_string();
+    };
+
+    let file_formatted = prettyplease::unparse(&file_unformatted);
+
+    let file_trimmed = file_formatted.trim();
+    let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
+    let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
+    let ty_formatted = end_removed.trim();
+
+    ty_formatted.to_string()
+}
+
+/// Represents the `#[deprecated]` attribute.
+///
+/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
+#[derive(Default)]
+pub struct DeprecatedAttribute {
+    pub since: Option<String>,
+    pub note: Option<String>,
+}
+
+impl DeprecatedAttribute {
+    /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
+    pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
+        if meta.path() != &parse_quote!(deprecated) {
+            return Err(syn::Error::new(
+                meta.span(),
+                "attribute path is not `deprecated`",
+            ));
+        }
+
+        match &meta {
+            Meta::Path(_) => Ok(Self::default()),
+            Meta::NameValue(name_value) => {
+                let Expr::Lit(expr_lit) = &name_value.value else {
+                    return Err(syn::Error::new(
+                        name_value.span(),
+                        "literal in `deprecated` value must be a string",
+                    ));
+                };
+
+                Ok(Self {
+                    since: None,
+                    note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
+                })
+            }
+            Meta::List(list) => {
+                let parsed = list.parse_args::<DeprecatedAttributeArgsParser>()?;
+
+                Ok(Self {
+                    since: parsed.since.map(|s| s.trim().to_string()),
+                    note: parsed.note.map(|s| s.trim().to_string()),
+                })
+            }
+        }
+    }
+}
+
+mod kw {
+    use syn::custom_keyword;
+    custom_keyword!(since);
+    custom_keyword!(note);
+}
+
+struct DeprecatedAttributeArgsParser {
+    since: Option<String>,
+    note: Option<String>,
+}
+
+impl Parse for DeprecatedAttributeArgsParser {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut since: Option<String> = None;
+        let mut note: Option<String> = None;
+
+        if input.peek(kw::since) {
+            input.parse::<kw::since>()?;
+            input.parse::<Token![=]>()?;
+
+            since = lit_to_string(input.parse()?);
+        }
+
+        if input.peek(Token![,]) && input.peek2(kw::note) {
+            input.parse::<Token![,]>()?;
+            input.parse::<kw::note>()?;
+            input.parse::<Token![=]>()?;
+
+            note = lit_to_string(input.parse()?);
+        }
+
+        Ok(Self { since, note })
+    }
+}

+ 3 - 9
packages/core/src/arena.rs

@@ -164,17 +164,11 @@ impl VirtualDom {
         });
 
         // Now that all the references are gone, we can safely drop our own references in our listeners.
-        let mut listeners = scope.attributes_to_drop.borrow_mut();
+        let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
         listeners.drain(..).for_each(|listener| {
             let listener = unsafe { &*listener };
-            match &listener.value {
-                AttributeValue::Listener(l) => {
-                    _ = l.take();
-                }
-                AttributeValue::Any(a) => {
-                    _ = a.take();
-                }
-                _ => (),
+            if let AttributeValue::Listener(l) = &listener.value {
+                _ = l.take();
             }
         });
     }

+ 41 - 3
packages/core/src/bump_frame.rs

@@ -1,10 +1,16 @@
 use crate::nodes::RenderReturn;
+use crate::{Attribute, AttributeValue, VComponent};
 use bumpalo::Bump;
+use std::cell::RefCell;
 use std::cell::{Cell, UnsafeCell};
 
 pub(crate) struct BumpFrame {
     pub bump: UnsafeCell<Bump>,
     pub node: Cell<*const RenderReturn<'static>>,
+
+    // The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
+    pub(crate) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
+    pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
 }
 
 impl BumpFrame {
@@ -13,6 +19,8 @@ impl BumpFrame {
         Self {
             bump: UnsafeCell::new(bump),
             node: Cell::new(std::ptr::null()),
+            attributes_to_drop_before_reset: Default::default(),
+            props_to_drop_before_reset: Default::default(),
         }
     }
 
@@ -31,8 +39,38 @@ impl BumpFrame {
         unsafe { &*self.bump.get() }
     }
 
-    #[allow(clippy::mut_from_ref)]
-    pub(crate) unsafe fn bump_mut(&self) -> &mut Bump {
-        unsafe { &mut *self.bump.get() }
+    pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
+        self.attributes_to_drop_before_reset
+            .borrow_mut()
+            .push(attribute);
+    }
+
+    /// Reset the bump allocator and drop all the attributes and props that were allocated in it.
+    ///
+    /// # Safety
+    /// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
+    pub(crate) unsafe fn reset(&self) {
+        let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
+        attributes.drain(..).for_each(|attribute| {
+            let attribute = unsafe { &*attribute };
+            if let AttributeValue::Any(l) = &attribute.value {
+                _ = l.take();
+            }
+        });
+        let mut props = self.props_to_drop_before_reset.borrow_mut();
+        props.drain(..).for_each(|prop| {
+            let prop = unsafe { &*prop };
+            _ = prop.props.borrow_mut().take();
+        });
+        unsafe {
+            let bump = &mut *self.bump.get();
+            bump.reset();
+        }
+    }
+}
+
+impl Drop for BumpFrame {
+    fn drop(&mut self) {
+        unsafe { self.reset() }
     }
 }

+ 1 - 1
packages/core/src/diff.rs

@@ -560,7 +560,7 @@ impl<'b> VirtualDom {
         // If none of the old keys are reused by the new children, then we remove all the remaining old children and
         // create the new children afresh.
         if shared_keys.is_empty() {
-            if old.get(0).is_some() {
+            if old.first().is_some() {
                 self.remove_nodes(&old[1..]);
                 self.replace(&old[0], new);
             } else {

+ 0 - 2
packages/core/src/events.rs

@@ -107,8 +107,6 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
     }
 }
 
-#[doc(hidden)]
-
 /// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
 ///
 /// This makes it possible to pass `move |evt| {}` style closures into components as property fields.

+ 31 - 2
packages/core/src/lazynodes.rs

@@ -23,8 +23,37 @@ use crate::{innerlude::VNode, ScopeState};
 ///
 ///
 /// ```rust, ignore
-/// LazyNodes::new(|f| f.element("div", [], [], [] None))
+/// LazyNodes::new(|f| {
+///        static TEMPLATE: dioxus::core::Template = dioxus::core::Template {
+///         name: "main.rs:5:5:20", // Source location of the template for hot reloading
+///         roots: &[
+///             dioxus::core::TemplateNode::Element {
+///                 tag: dioxus_elements::div::TAG_NAME,
+///                 namespace: dioxus_elements::div::NAME_SPACE,
+///                 attrs: &[],
+///                 children: &[],
+///             },
+///         ],
+///         node_paths: &[],
+///         attr_paths: &[],
+///     };
+///     dioxus::core::VNode {
+///         parent: None,
+///         key: None,
+///         template: std::cell::Cell::new(TEMPLATE),
+///         root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(
+///                 1usize,
+///                 f.bump(),
+///             )
+///             .into(),
+///         dynamic_nodes: f.bump().alloc([]),
+///         dynamic_attrs: f.bump().alloc([]),
+///     })
+/// }
 /// ```
+///
+/// Find more information about how to construct [`VNode`] at <https://dioxuslabs.com/learn/0.4/contributing/walkthrough_readme#the-rsx-macro>
+
 pub struct LazyNodes<'a, 'b> {
     #[cfg(not(miri))]
     inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
@@ -61,7 +90,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
     /// Call the closure with the given factory to produce real [`VNode`].
     ///
     /// ```rust, ignore
-    /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
+    /// let f = LazyNodes::new(/* Closure for creating VNodes */);
     ///
     /// let node = f.call(cac);
     /// ```

+ 3 - 3
packages/core/src/lib.rs

@@ -91,9 +91,9 @@ pub mod prelude {
         consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
         provide_context, provide_context_to_scope, provide_root_context, push_future,
         remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
-        Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
-        Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
-        TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
+        Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
+        LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
+        Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
     };
 }
 

+ 1 - 1
packages/core/src/mutations.rs

@@ -91,7 +91,7 @@ pub enum Mutation<'a> {
         id: ElementId,
     },
 
-    /// Create an placeholder int he DOM that we will use later.
+    /// Create a placeholder in the DOM that we will use later.
     ///
     /// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
     CreatePlaceholder {

+ 7 - 1
packages/core/src/nodes.rs

@@ -707,7 +707,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
 impl IntoDynNode<'_> for String {
     fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
         DynamicNode::Text(VText {
-            value: cx.bump().alloc(self),
+            value: cx.bump().alloc_str(&self),
             id: Default::default(),
         })
     }
@@ -791,6 +791,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
     }
 }
 
+impl<'a> IntoAttributeValue<'a> for String {
+    fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
+        AttributeValue::Text(cx.alloc_str(&self))
+    }
+}
+
 impl<'a> IntoAttributeValue<'a> for f64 {
     fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
         AttributeValue::Float(self)

+ 2 - 2
packages/core/src/scope_arena.rs

@@ -35,7 +35,7 @@ impl VirtualDom {
             hook_idx: Default::default(),
 
             borrowed_props: Default::default(),
-            attributes_to_drop: Default::default(),
+            attributes_to_drop_before_render: Default::default(),
         }));
 
         let context =
@@ -54,7 +54,7 @@ impl VirtualDom {
 
         let new_nodes = unsafe {
             let scope = &self.scopes[scope_id.0];
-            scope.previous_frame().bump_mut().reset();
+            scope.previous_frame().reset();
 
             scope.context().suspended.set(false);
 

+ 15 - 4
packages/core/src/scopes.rs

@@ -94,7 +94,7 @@ pub struct ScopeState {
     pub(crate) hook_idx: Cell<usize>,
 
     pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
-    pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
+    pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
 
     pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
 }
@@ -348,25 +348,36 @@ impl<'src> ScopeState {
     pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
         let element = rsx.call(self);
 
-        let mut listeners = self.attributes_to_drop.borrow_mut();
+        let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
         for attr in element.dynamic_attrs {
             match attr.value {
-                AttributeValue::Any(_) | AttributeValue::Listener(_) => {
+                // We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
+                AttributeValue::Listener(_) => {
                     let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
                     listeners.push(unbounded);
                 }
+                // We need to drop any values manually to make sure that their drop implementation is called before the next render
+                AttributeValue::Any(_) => {
+                    let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
+                    self.previous_frame().add_attribute_to_drop(unbounded);
+                }
 
                 _ => (),
             }
         }
 
         let mut props = self.borrowed_props.borrow_mut();
+        let mut drop_props = self
+            .previous_frame()
+            .props_to_drop_before_reset
+            .borrow_mut();
         for node in element.dynamic_nodes {
             if let DynamicNode::Component(comp) = node {
+                let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
                 if !comp.static_props {
-                    let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
                     props.push(unbounded);
                 }
+                drop_props.push(unbounded);
             }
         }
 

+ 1 - 0
packages/desktop/Cargo.toml

@@ -59,6 +59,7 @@ devtools = ["wry/devtools"]
 tray = ["wry/tray"]
 dox = ["wry/dox"]
 hot-reload = ["dioxus-hot-reload"]
+gnu = []
 
 [package.metadata.docs.rs]
 default-features = false

+ 9 - 0
packages/desktop/build.rs

@@ -0,0 +1,9 @@
+fn main() {
+    // WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
+    if std::env::var("CARGO_CFG_WINDOWS").is_ok()
+        && std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
+        && !cfg!(feature = "gnu")
+    {
+        println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
+    }
+}

+ 13 - 4
packages/desktop/src/lib.rs

@@ -54,7 +54,7 @@ use wry::{application::window::WindowId, webview::WebContext};
 ///
 /// This function will start a multithreaded Tokio runtime as well the WebView event loop.
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// use dioxus::prelude::*;
 ///
 /// fn main() {
@@ -77,11 +77,12 @@ pub fn launch(root: Component) {
 ///
 /// You can configure the WebView window with a configuration closure
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// use dioxus::prelude::*;
+/// use dioxus_desktop::*;
 ///
 /// fn main() {
-///     dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App")));
+///     dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
 /// }
 ///
 /// fn app(cx: Scope) -> Element {
@@ -100,8 +101,9 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
 ///
 /// You can configure the WebView window with a configuration closure
 ///
-/// ```rust, ignore
+/// ```rust, no_run
 /// use dioxus::prelude::*;
+/// use dioxus_desktop::Config;
 ///
 /// fn main() {
 ///     dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
@@ -161,6 +163,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
     // iOS panics if we create a window before the event loop is started
     let props = Rc::new(Cell::new(Some(props)));
     let cfg = Rc::new(Cell::new(Some(cfg)));
+    let mut is_visible_before_start = true;
 
     event_loop.run(move |window_event, event_loop, control_flow| {
         *control_flow = ControlFlow::Wait;
@@ -210,6 +213,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
                 // Create a dom
                 let dom = VirtualDom::new_with_props(root, props);
 
+                is_visible_before_start = cfg.window.window.visible;
+
                 let handler = create_new_window(
                     cfg,
                     event_loop,
@@ -323,6 +328,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
                 EventData::Ipc(msg) if msg.method() == "initialize" => {
                     let view = webviews.get_mut(&event.1).unwrap();
                     send_edits(view.dom.rebuild(), &view.desktop_context.webview);
+                    view.desktop_context
+                        .webview
+                        .window()
+                        .set_visible(is_visible_before_start);
                 }
 
                 EventData::Ipc(msg) if msg.method() == "browser_open" => {

+ 1 - 1
packages/desktop/src/protocol.rs

@@ -153,7 +153,7 @@ fn get_asset_root() -> Option<PathBuf> {
 
 /// Get the mime type from a path-like string
 fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
-    if trimmed.ends_with(".svg") {
+    if trimmed.extension().is_some_and(|ext| ext == "svg") {
         return Ok("image/svg+xml");
     }
 

+ 1 - 1
packages/desktop/src/webview.rs

@@ -13,7 +13,7 @@ pub fn build(
     proxy: EventLoopProxy<UserWindowEvent>,
 ) -> (WebView, WebContext) {
     let builder = cfg.window.clone();
-    let window = builder.build(event_loop).unwrap();
+    let window = builder.with_visible(false).build(event_loop).unwrap();
     let file_handler = cfg.file_drop_handler.take();
     let custom_head = cfg.custom_head.clone();
     let index_file = cfg.custom_index.clone();

+ 6 - 6
packages/dioxus-tui/examples/colorpicker.rs

@@ -15,21 +15,21 @@ fn app(cx: Scope) -> Element {
     let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
     // disable templates so that every node has an id and can be queried
     cx.render(rsx! {
-        div{
+        div {
             width: "100%",
             background_color: "hsl({hue}, 70%, {brightness}%)",
             onmousemove: move |evt| {
                 if let RenderReturn::Ready(node) = cx.root_node() {
-                    if let Some(id) = node.root_ids.borrow().get(0).cloned() {
+                    if let Some(id) = node.root_ids.borrow().first().cloned() {
                         let node = tui_query.get(mapping.get_node_id(id).unwrap());
-                        let Size{width, height} = node.size().unwrap();
+                        let Size { width, height } = node.size().unwrap();
                         let pos = evt.inner().element_coordinates();
-                        hue.set((pos.x as f32/width as f32)*255.0);
-                        brightness.set((pos.y as f32/height as f32)*100.0);
+                        hue.set((pos.x as f32 / width as f32) * 255.0);
+                        brightness.set((pos.y as f32 / height as f32) * 100.0);
                     }
                 }
             },
-            "hsl({hue}, 70%, {brightness}%)",
+            "hsl({hue}, 70%, {brightness}%)"
         }
     })
 }

+ 39 - 7
packages/extension/src/lib.rs

@@ -1,17 +1,39 @@
 //! This file exports functions into the vscode extension
 
-use dioxus_autofmt::FormattedBlock;
+use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType};
 use wasm_bindgen::prelude::*;
 
 #[wasm_bindgen]
-pub fn format_rsx(raw: String) -> String {
-    let block = dioxus_autofmt::fmt_block(&raw, 0);
+pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
+    let block = dioxus_autofmt::fmt_block(
+        &raw,
+        0,
+        IndentOptions::new(
+            if use_tabs {
+                IndentType::Tabs
+            } else {
+                IndentType::Spaces
+            },
+            indent_size,
+        ),
+    );
     block.unwrap()
 }
 
 #[wasm_bindgen]
-pub fn format_selection(raw: String) -> String {
-    let block = dioxus_autofmt::fmt_block(&raw, 0);
+pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
+    let block = dioxus_autofmt::fmt_block(
+        &raw,
+        0,
+        IndentOptions::new(
+            if use_tabs {
+                IndentType::Tabs
+            } else {
+                IndentType::Spaces
+            },
+            indent_size,
+        ),
+    );
     block.unwrap()
 }
 
@@ -35,8 +57,18 @@ impl FormatBlockInstance {
 }
 
 #[wasm_bindgen]
-pub fn format_file(contents: String) -> FormatBlockInstance {
-    let _edits = dioxus_autofmt::fmt_file(&contents);
+pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
+    let _edits = dioxus_autofmt::fmt_file(
+        &contents,
+        IndentOptions::new(
+            if use_tabs {
+                IndentType::Tabs
+            } else {
+                IndentType::Spaces
+            },
+            indent_size,
+        ),
+    );
     let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
     FormatBlockInstance { new: out, _edits }
 }

+ 7 - 1
packages/extension/src/main.ts

@@ -90,7 +90,13 @@ function fmtDocument(document: vscode.TextDocument) {
 		if (!editor) return; // Need an editor to apply text edits.
 
 		const contents = editor.document.getText();
-		const formatted = dioxus.format_file(contents);
+		let tabSize: number;
+		if (typeof editor.options.tabSize === 'number') {
+			tabSize = editor.options.tabSize;
+		} else {
+			tabSize = 4;
+		}
+		const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, tabSize);
 
 		// Replace the entire text document
 		// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using

+ 1 - 1
packages/fermi/src/hooks/atom_root.rs

@@ -7,6 +7,6 @@ use dioxus_core::ScopeState;
 pub fn use_atom_root(cx: &ScopeState) -> &Rc<AtomRoot> {
     cx.use_hook(|| match cx.consume_context::<Rc<AtomRoot>>() {
         Some(root) => root,
-        None => panic!("No atom root found in context. Did you forget place an AtomRoot component at the top of your app?"),
+        None => panic!("No atom root found in context. Did you forget to call use_init_atom_root at the top of your app?"),
     })
 }

+ 3 - 1
packages/fermi/src/hooks/state.rs

@@ -86,7 +86,9 @@ impl<T: 'static> AtomState<T> {
     /// ```
     #[must_use]
     pub fn current(&self) -> Rc<T> {
-        self.value.as_ref().unwrap().clone()
+        let atoms = self.root.atoms.borrow();
+        let slot = atoms.get(&self.id).unwrap();
+        slot.value.clone().downcast().unwrap()
     }
 
     /// Get the `setter` function directly without the `AtomState` wrapper.

+ 0 - 2
packages/fermi/src/lib.rs

@@ -22,8 +22,6 @@ mod atoms {
     pub use atom::*;
     pub use atomfamily::*;
     pub use atomref::*;
-    pub use selector::*;
-    pub use selectorfamily::*;
 }
 
 pub mod hooks {

+ 1 - 1
packages/fullstack/Cargo.toml

@@ -11,7 +11,7 @@ keywords = ["ui", "gui", "react", "ssr", "fullstack"]
 
 [dependencies]
 # server functions
-server_fn = { version = "0.4.6", default-features = false }
+server_fn = { version = "0.5.2", default-features = false }
 dioxus_server_macro = { workspace = true }
 
 # warp

+ 1 - 0
packages/fullstack/examples/axum-hello-world/src/main.rs

@@ -24,6 +24,7 @@ fn app(cx: Scope<AppProps>) -> Element {
 
     let mut count = use_state(cx, || 0);
     let text = use_state(cx, || "...".to_string());
+    let eval = use_eval(cx);
 
     cx.render(rsx! {
         div {

+ 62 - 4
packages/fullstack/src/adapters/axum_adapter.rs

@@ -369,15 +369,65 @@ fn apply_request_parts_to_response<B>(
     }
 }
 
-/// SSR renderer handler for Axum
-pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
-    State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
+/// SSR renderer handler for Axum with added context injection.
+///
+/// # Example
+/// ```rust,no_run
+/// #![allow(non_snake_case)]
+/// use std::sync::{Arc, Mutex};
+///
+/// use axum::routing::get;
+/// use dioxus::prelude::*;
+/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*};
+///
+/// fn app(cx: Scope) -> Element {
+///     render! {
+///         "hello!"
+///     }
+/// }
+///
+/// #[tokio::main]
+/// async fn main() {
+///     let cfg = ServeConfigBuilder::new(app, ())
+///         .assets_path("dist")
+///         .build();
+///     let ssr_state = SSRState::new(&cfg);
+///
+///     // This could be any state you want to be accessible from your server
+///     // functions using `[DioxusServerContext::get]`.
+///     let state = Arc::new(Mutex::new("state".to_string()));
+///
+///     let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
+///     axum::Server::bind(&addr)
+///         .serve(
+///             axum::Router::new()
+///                 // Register server functions, etc.
+///                 // Note you probably want to use `register_server_fns_with_handler`
+///                 // to inject the context into server functions running outside
+///                 // of an SSR render context.
+///                 .fallback(get(render_handler_with_context).with_state((
+///                     move |ctx| ctx.insert(state.clone()).unwrap(),
+///                     cfg,
+///                     ssr_state,
+///                 )))
+///                 .into_make_service(),
+///         )
+///         .await
+///         .unwrap();
+/// }
+/// ```
+pub async fn render_handler_with_context<
+    P: Clone + serde::Serialize + Send + Sync + 'static,
+    F: FnMut(&mut DioxusServerContext),
+>(
+    State((mut inject_context, cfg, ssr_state)): State<(F, ServeConfig<P>, SSRState)>,
     request: Request<Body>,
 ) -> impl IntoResponse {
     let (parts, _) = request.into_parts();
     let url = parts.uri.path_and_query().unwrap().to_string();
     let parts: Arc<RwLock<http::request::Parts>> = Arc::new(RwLock::new(parts.into()));
-    let server_context = DioxusServerContext::new(parts.clone());
+    let mut server_context = DioxusServerContext::new(parts.clone());
+    inject_context(&mut server_context);
 
     match ssr_state.render(url, &cfg, &server_context).await {
         Ok(rendered) => {
@@ -395,6 +445,14 @@ pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>
     }
 }
 
+/// SSR renderer handler for Axum
+pub async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
+    State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
+    request: Request<Body>,
+) -> impl IntoResponse {
+    render_handler_with_context(State((|_: &mut _| (), cfg, ssr_state)), request).await
+}
+
 fn report_err<E: std::fmt::Display>(e: E) -> Response<BoxBody> {
     Response::builder()
         .status(StatusCode::INTERNAL_SERVER_ERROR)

+ 19 - 19
packages/fullstack/src/adapters/mod.rs

@@ -89,26 +89,26 @@ impl Service for ServerFnHandler {
             let parts = Arc::new(RwLock::new(parts));
 
             // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
-            let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
             let pool = get_local_pool();
-            pool.spawn_pinned({
-                let function = function.clone();
-                let mut server_context = server_context.clone();
-                server_context.parts = parts;
-                move || async move {
-                    let data = match function.encoding() {
-                        Encoding::Url | Encoding::Cbor => &body,
-                        Encoding::GetJSON | Encoding::GetCBOR => &query,
-                    };
-                    let server_function_future = function.call((), data);
-                    let server_function_future =
-                        ProvideServerContext::new(server_function_future, server_context.clone());
-                    let resp = server_function_future.await;
-
-                    resp_tx.send(resp).unwrap();
-                }
-            });
-            let result = resp_rx.await.unwrap();
+            let result = pool
+                .spawn_pinned({
+                    let function = function.clone();
+                    let mut server_context = server_context.clone();
+                    server_context.parts = parts;
+                    move || async move {
+                        let data = match function.encoding() {
+                            Encoding::Url | Encoding::Cbor => &body,
+                            Encoding::GetJSON | Encoding::GetCBOR => &query,
+                        };
+                        let server_function_future = function.call((), data);
+                        let server_function_future = ProvideServerContext::new(
+                            server_function_future,
+                            server_context.clone(),
+                        );
+                        server_function_future.await
+                    }
+                })
+                .await?;
             let mut res = http::Response::builder();
 
             // Set the headers from the server context

+ 9 - 2
packages/fullstack/src/launch.rs

@@ -121,8 +121,15 @@ impl<Props: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync
     #[cfg(feature = "web")]
     /// Launch the web application
     pub fn launch_web(self) {
-        let cfg = self.web_cfg.hydrate(true);
-        dioxus_web::launch_with_props(self.component, get_root_props_from_document().unwrap(), cfg);
+        #[cfg(not(feature = "ssr"))]
+        {
+            let cfg = self.web_cfg.hydrate(true);
+            dioxus_web::launch_with_props(
+                self.component,
+                get_root_props_from_document().unwrap(),
+                cfg,
+            );
+        }
     }
 
     #[cfg(feature = "desktop")]

+ 5 - 0
packages/fullstack/src/layer.rs

@@ -3,7 +3,9 @@ use tracing_futures::Instrument;
 
 use http::{Request, Response};
 
+/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service
 pub trait Layer: Send + Sync + 'static {
+    /// Wrap a boxed service with this layer
     fn layer(&self, inner: BoxedService) -> BoxedService;
 }
 
@@ -17,7 +19,9 @@ where
     }
 }
 
+/// A service is a function that takes a request and returns an async response
 pub trait Service {
+    /// Run the service and produce a future that resolves to a response
     fn run(
         &mut self,
         req: http::Request<hyper::body::Body>,
@@ -55,6 +59,7 @@ where
     }
 }
 
+/// A boxed service is a type-erased service that can be used without knowing the underlying type
 pub struct BoxedService(pub Box<dyn Service + Send>);
 
 impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.