Browse Source

Merge branch 'master' into intigrate-collect-assets

Evan Almloff 1 năm trước cách đây
mục cha
commit
66e2c02bf7
100 tập tin đã thay đổi với 1970 bổ sung619 xóa
  1. 5 0
      .cargo/config.toml
  2. 1 1
      .github/workflows/docs stable.yml
  3. 1 1
      .github/workflows/docs.yml
  4. 5 1
      .github/workflows/main.yml
  5. 1 2
      .github/workflows/miri.yml
  6. 1 1
      .github/workflows/playwright.yml
  7. 0 171
      CHANGELOG.md
  8. 5 4
      Cargo.toml
  9. 70 4
      Makefile.toml
  10. 1 0
      README.md
  11. 1 0
      examples/calculator.rs
  12. 3 0
      examples/openid_connect_demo/.gitignore
  13. 25 0
      examples/openid_connect_demo/Cargo.toml
  14. 47 0
      examples/openid_connect_demo/Dioxus.toml
  15. 13 0
      examples/openid_connect_demo/README.md
  16. 2 0
      examples/openid_connect_demo/src/constants.rs
  17. 20 0
      examples/openid_connect_demo/src/errors.rs
  18. 60 0
      examples/openid_connect_demo/src/main.rs
  19. 1 0
      examples/openid_connect_demo/src/model/mod.rs
  20. 7 0
      examples/openid_connect_demo/src/model/user.rs
  21. 125 0
      examples/openid_connect_demo/src/oidc.rs
  22. 20 0
      examples/openid_connect_demo/src/props/client.rs
  23. 1 0
      examples/openid_connect_demo/src/props/mod.rs
  24. 17 0
      examples/openid_connect_demo/src/router.rs
  25. 38 0
      examples/openid_connect_demo/src/storage.rs
  26. 250 0
      examples/openid_connect_demo/src/views/header.rs
  27. 5 0
      examples/openid_connect_demo/src/views/home.rs
  28. 86 0
      examples/openid_connect_demo/src/views/login.rs
  29. 4 0
      examples/openid_connect_demo/src/views/mod.rs
  30. 7 0
      examples/openid_connect_demo/src/views/not_found.rs
  31. 12 0
      examples/optional_props.rs
  32. 1 0
      examples/query_segments_demo/Cargo.toml
  33. 23 1
      examples/signals.rs
  34. 1 1
      examples/tailwind/Cargo.toml
  35. 114 68
      examples/todomvc.rs
  36. 6 5
      packages/autofmt/src/buffer.rs
  37. 6 6
      packages/autofmt/src/element.rs
  38. 3 3
      packages/autofmt/src/expr.rs
  39. 108 0
      packages/autofmt/src/indent.rs
  40. 12 15
      packages/autofmt/src/lib.rs
  41. 2 2
      packages/autofmt/src/writer.rs
  42. 1 1
      packages/autofmt/tests/samples.rs
  43. 10 5
      packages/autofmt/tests/wrong.rs
  44. 0 0
      packages/autofmt/tests/wrong/comments-4sp.rsx
  45. 0 0
      packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
  46. 7 0
      packages/autofmt/tests/wrong/comments-tab.rsx
  47. 5 0
      packages/autofmt/tests/wrong/comments-tab.wrong.rsx
  48. 0 0
      packages/autofmt/tests/wrong/multi-4sp.rsx
  49. 0 0
      packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
  50. 3 0
      packages/autofmt/tests/wrong/multi-tab.rsx
  51. 5 0
      packages/autofmt/tests/wrong/multi-tab.wrong.rsx
  52. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.rsx
  53. 0 0
      packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
  54. 8 0
      packages/autofmt/tests/wrong/multiexpr-tab.rsx
  55. 5 0
      packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
  56. 1 1
      packages/cli/Cargo.toml
  57. 1 1
      packages/cli/README.md
  58. 1 1
      packages/cli/src/assets/dioxus.toml
  59. 16 4
      packages/cli/src/builder.rs
  60. 62 6
      packages/cli/src/cli/autoformat.rs
  61. 4 4
      packages/cli/src/cli/build.rs
  62. 1 1
      packages/cli/src/config.rs
  63. 3 0
      packages/cli/src/error.rs
  64. 13 1
      packages/cli/src/logging.rs
  65. 30 41
      packages/cli/src/main.rs
  66. 13 11
      packages/cli/src/server/desktop/mod.rs
  67. 17 7
      packages/cli/src/server/mod.rs
  68. 14 11
      packages/cli/src/server/output.rs
  69. 1 0
      packages/core-macro/Cargo.toml
  70. 1 0
      packages/core-macro/src/component_body_deserializers/component.rs
  71. 292 146
      packages/core-macro/src/component_body_deserializers/inline_props.rs
  72. 1 0
      packages/core-macro/src/lib.rs
  73. 24 40
      packages/core-macro/src/props/mod.rs
  74. 129 0
      packages/core-macro/src/utils.rs
  75. 3 9
      packages/core/src/arena.rs
  76. 41 3
      packages/core/src/bump_frame.rs
  77. 1 1
      packages/core/src/diff.rs
  78. 0 2
      packages/core/src/events.rs
  79. 31 2
      packages/core/src/lazynodes.rs
  80. 3 3
      packages/core/src/lib.rs
  81. 1 1
      packages/core/src/mutations.rs
  82. 7 1
      packages/core/src/nodes.rs
  83. 2 2
      packages/core/src/scope_arena.rs
  84. 15 4
      packages/core/src/scopes.rs
  85. 1 0
      packages/desktop/Cargo.toml
  86. 9 0
      packages/desktop/build.rs
  87. 13 4
      packages/desktop/src/lib.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 0
      packages/fermi/src/callback.rs
  93. 1 0
      packages/fermi/src/hooks/atom_ref.rs
  94. 1 1
      packages/fermi/src/hooks/atom_root.rs
  95. 2 0
      packages/fermi/src/hooks/read.rs
  96. 1 0
      packages/fermi/src/hooks/set.rs
  97. 4 1
      packages/fermi/src/hooks/state.rs
  98. 0 2
      packages/fermi/src/lib.rs
  99. 1 1
      packages/fullstack/Cargo.toml
  100. 1 0
      packages/fullstack/examples/axum-hello-world/src/main.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 - 1
.github/workflows/main.yml

@@ -130,7 +130,6 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - uses: ilammy/setup-nasm@v1
-
       - name: install stable
         uses: dtolnay/rust-toolchain@master
         with:
@@ -146,6 +145,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

@@ -87,8 +87,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

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

@@ -21,7 +21,7 @@ jobs:
       # Do our best to cache the toolchain and node install steps
       - uses: actions/checkout@v4
       - uses: ilammy/setup-nasm@v1
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: 16
       - name: Install Rust

+ 0 - 171
CHANGELOG.md

@@ -1,171 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-## Unreleased
-
-### Commit Statistics
-
-<csr-read-only-do-not-edit/>
-
- - 1 commit contributed to the release over the course of 7 calendar days.
- - 0 commits where understood as [conventional](https://www.conventionalcommits.org).
- - 0 issues like '(#ID)' where seen in commit messages
-
-### Commit Details
-
-<csr-read-only-do-not-edit/>
-
-<details><summary>view details</summary>
-
- * **Uncategorized**
-    - Fix various typos and grammar nits ([`9e4ec43`](https://github.comgit//DioxusLabs/dioxus/commit/9e4ec43b1e78d355c56a38e4c092170b2b01b20d))
-</details>
-
-## v0.1.7 (2022-01-08)
-
-### Bug Fixes
-
- - <csr-id-bd341f5571580cdf5e495379b49ca988fd9211c3/> tests
-
-### Commit Statistics
-
-<csr-read-only-do-not-edit/>
-
- - 1 commit contributed to the release over the course of 2 calendar days.
- - 1 commit where understood as [conventional](https://www.conventionalcommits.org).
- - 0 issues like '(#ID)' where seen in commit messages
-
-### Commit Details
-
-<csr-read-only-do-not-edit/>
-
-<details><summary>view details</summary>
-
- * **Uncategorized**
-    - tests ([`bd341f5`](https://github.comgit//DioxusLabs/dioxus/commit/bd341f5571580cdf5e495379b49ca988fd9211c3))
-</details>
-
-## v0.1.1 (2021-12-15)
-
-### Documentation
-
- - <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- - <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
- - <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- - <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
- - <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
-
-### New Features
-
- - <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- - <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- - <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- - <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
- - <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- - <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- - <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- - <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
-
-## v0.1.0 (2021-12-15)
-
-### Documentation
-
- - <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- - <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
- - <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- - <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
- - <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
-
-### New Features
-
- - <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- - <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- - <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- - <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
- - <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- - <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- - <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- - <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
-
-## v0.0.1 (2022-01-03)
-
-### Documentation
-
- - <csr-id-78007445f944f259170307d840e0f16242b7b4b6/> improve docs
- - <csr-id-4de16c4779648e591b3869b5df31271ae603c812/> update local examples and docs to support new syntaxes
- - <csr-id-583fdfa5618e11d660985b97e570d4503be2ff49/> big updates to the reference
- - <csr-id-bf21c82de04e25daee60a06232b9a16b640508f2/> lib.rs docs
- - <csr-id-70cd46dbb2a689ae2d512e142b8aee9c80798430/> move around examples
-
-### New Features
-
- - <csr-id-8acdd2ea830b995b608d8bac2ef527db8d40e662/> it compiles once more
- - <csr-id-9726a065b0d4fb1ede5b53a2ddd58c855e51539f/> massage lifetimes
- - <csr-id-4a72b3140bd244da602deada1eeecded65ff5848/> amazingly awesome error handling
- - <csr-id-3bedcb93cacec5bdf134adc38ff02eadbf96c1c6/> svgs working in webview
- - <csr-id-a2c7d17b0595769f60bc1c2bbf7cbe32cec37486/> mvoe away from compound context
- - <csr-id-de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2/> more suspended nodes!
- - <csr-id-4091846934b4b3b2bc03d3ca8aaf7712aebd4e36/> add aria
- - <csr-id-7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2/> enable arbitrary body in rsx! macro
-
-### Commit Statistics
-
-<csr-read-only-do-not-edit/>
-
- - 40 commits contributed to the release over the course of 193 calendar days.
- - 38 commits where understood as [conventional](https://www.conventionalcommits.org).
- - 0 issues like '(#ID)' where seen in commit messages
-
-### Commit Details
-
-<csr-read-only-do-not-edit/>
-
-<details><summary>view details</summary>
-
- * **Uncategorized**
-    - remove runner on hook and then update docs ([`d156045`](https://github.comgit//DioxusLabs/dioxus/commit/d1560450bac55f9566e00e00ea405bd1c70b57e5))
-    - polish some more things ([`1496102`](https://github.comgit//DioxusLabs/dioxus/commit/14961023f927b3a8bde83cfc7883aa8bfcca9e85))
-    - upgrade hooks ([`b3ac2ee`](https://github.comgit//DioxusLabs/dioxus/commit/b3ac2ee3f76549cd1c7b6f9eee7e3382b07d873c))
-    - docs ([`8814977`](https://github.comgit//DioxusLabs/dioxus/commit/8814977eeebe06748a3b9677a8070e42a037ebd7))
-    - prepare to change our fragment pattern. Add some more docs ([`2c3a046`](https://github.comgit//DioxusLabs/dioxus/commit/2c3a0464264fa11e8100df025d863931f9606cdb))
-    - it compiles once more ([`8acdd2e`](https://github.comgit//DioxusLabs/dioxus/commit/8acdd2ea830b995b608d8bac2ef527db8d40e662))
-    - some docs and suspense ([`93d4b8c`](https://github.comgit//DioxusLabs/dioxus/commit/93d4b8ca7c1b133e5dba2a8dc9a310dbe1357001))
-    - docs and router ([`a5f05d7`](https://github.comgit//DioxusLabs/dioxus/commit/a5f05d73acc0e47b05cff64a373482519414bc7c))
-    - Merge branch 'master' into jk/remove_node_safety ([`db00047`](https://github.comgit//DioxusLabs/dioxus/commit/db0004758c77331cc3b93ea8cf227c060028e12e))
-    - improve docs ([`7800744`](https://github.comgit//DioxusLabs/dioxus/commit/78007445f944f259170307d840e0f16242b7b4b6))
-    - Various typos/grammar/rewording ([`5747e00`](https://github.comgit//DioxusLabs/dioxus/commit/5747e00b27b1b69c4f9c2820e7e78030feaff71e))
-    - bubbling in progress ([`a21020e`](https://github.comgit//DioxusLabs/dioxus/commit/a21020ea575e467ba0d608737269fe1b0792dba7))
-    - update local examples and docs to support new syntaxes ([`4de16c4`](https://github.comgit//DioxusLabs/dioxus/commit/4de16c4779648e591b3869b5df31271ae603c812))
-    - massage lifetimes ([`9726a06`](https://github.comgit//DioxusLabs/dioxus/commit/9726a065b0d4fb1ede5b53a2ddd58c855e51539f))
-    - major cleanups to scheduler ([`2933e4b`](https://github.comgit//DioxusLabs/dioxus/commit/2933e4bc11b3074c2bde8d76ec55364fca841988))
-    - threadsafe ([`82953f2`](https://github.comgit//DioxusLabs/dioxus/commit/82953f2ac37913f83a822333acd0c47e20777d31))
-    - move macro crate out of core ([`7bdad1e`](https://github.comgit//DioxusLabs/dioxus/commit/7bdad1e2e6f67e74c9f67dde2150140cf8a090e8))
-    - amazingly awesome error handling ([`4a72b31`](https://github.comgit//DioxusLabs/dioxus/commit/4a72b3140bd244da602deada1eeecded65ff5848))
-    - some ideas ([`05c909f`](https://github.comgit//DioxusLabs/dioxus/commit/05c909f320765aec1bf4c1c55ca59ffd5525a2c7))
-    - big updates to the reference ([`583fdfa`](https://github.comgit//DioxusLabs/dioxus/commit/583fdfa5618e11d660985b97e570d4503be2ff49))
-    - docs, html! macro, more ([`caf772c`](https://github.comgit//DioxusLabs/dioxus/commit/caf772cf249d2f56c8d0b0fa2737ad48e32c6e82))
-    - cleanup workspace ([`8f0bb5d`](https://github.comgit//DioxusLabs/dioxus/commit/8f0bb5dc5bfa3e775af567c4b569622cdd932af1))
-    - svgs working in webview ([`3bedcb9`](https://github.comgit//DioxusLabs/dioxus/commit/3bedcb93cacec5bdf134adc38ff02eadbf96c1c6))
-    - mvoe away from compound context ([`a2c7d17`](https://github.comgit//DioxusLabs/dioxus/commit/a2c7d17b0595769f60bc1c2bbf7cbe32cec37486))
-    - more suspended nodes! ([`de9f61b`](https://github.comgit//DioxusLabs/dioxus/commit/de9f61bcf48c0d6e35e46c337b72a713c9f9f7d2))
-    - add aria ([`4091846`](https://github.comgit//DioxusLabs/dioxus/commit/4091846934b4b3b2bc03d3ca8aaf7712aebd4e36))
-    - more examples ([`56e7eb8`](https://github.comgit//DioxusLabs/dioxus/commit/56e7eb83a97ebd6d5bcd23464cfb9d718e5ac26d))
-    - more refactor for async ([`975fa56`](https://github.comgit//DioxusLabs/dioxus/commit/975fa566f9809f8fa2bb0bdb07fbfc7f855dcaeb))
-    - enable arbitrary body in rsx! macro ([`7aec40d`](https://github.comgit//DioxusLabs/dioxus/commit/7aec40d57e78ec13ff3a90ca8149521cbf1d9ff2))
-    - move CLI into its own "studio" app ([`fd79335`](https://github.comgit//DioxusLabs/dioxus/commit/fd7933561fe81922e4d5d77f6ac3b6f19efb5a90))
-    - move some examples around ([`98a0933`](https://github.comgit//DioxusLabs/dioxus/commit/98a09339fd3190799ea4dd316908f0a53fdf2413))
-    - fix issues with lifetimes ([`a38a81e`](https://github.comgit//DioxusLabs/dioxus/commit/a38a81e1290375cae685f7c49d3745e4298fab26))
-    - more examples ([`11f89e5`](https://github.comgit//DioxusLabs/dioxus/commit/11f89e5d338d14a7aeece0a6275c24ae65913ce7))
-    - lib.rs docs ([`bf21c82`](https://github.comgit//DioxusLabs/dioxus/commit/bf21c82de04e25daee60a06232b9a16b640508f2))
-    - rename ctx to cx ([`81382e7`](https://github.comgit//DioxusLabs/dioxus/commit/81382e7044fb3dba61d4abb1e6086b7b29143116))
-    - move around examples ([`70cd46d`](https://github.comgit//DioxusLabs/dioxus/commit/70cd46dbb2a689ae2d512e142b8aee9c80798430))
-    - start moving events to rc<event> ([`b9ff95f`](https://github.comgit//DioxusLabs/dioxus/commit/b9ff95fa12c46365fe73b64a4926a506d5da2342))
-    - rename recoil to atoms ([`36ea39a`](https://github.comgit//DioxusLabs/dioxus/commit/36ea39ae30aa3f1fb2d718c0fdf08850c6bfd3ac))
-    - more examples and docs ([`7fbaf69`](https://github.comgit//DioxusLabs/dioxus/commit/7fbaf69cabbdde712bb3fd9e4b2a5dc18b9390e9))
-    - docs ([`f5683a2`](https://github.comgit//DioxusLabs/dioxus/commit/f5683a23464992ecace463a61414795b5a2c58c8))
-</details>
-

+ 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

+ 1 - 0
README.md

@@ -159,6 +159,7 @@ 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!
 

+ 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 }
+	})
+}

+ 1 - 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"

+ 1 - 1
packages/cli/README.md

@@ -11,7 +11,7 @@ It handles building, bundling, development and publishing to simplify developmen
 ### Install the stable version (recommended)
 
 ```
-cargo install dioxus-cli --locked
+cargo install dioxus-cli
 ```
 
 ### Install the latest development build through git

+ 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]

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

@@ -49,14 +49,25 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
 
     // [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")
@@ -66,7 +77,7 @@ pub fn build(config: &CrateConfig, _: bool, skip_assets: bool) -> Result<BuildRe
     let cmd = if config.verbose {
         cmd.arg("--verbose")
     } else {
-        cmd
+        cmd.arg("--quiet")
     };
 
     let cmd = if config.custom_profile.is_some() {
@@ -263,6 +274,7 @@ pub fn build_desktop(
     let mut cmd = subprocess::Exec::cmd("cargo")
         .cwd(&config.crate_dir)
         .arg("build")
+        .arg("--quiet")
         .arg("--message-format=json");
 
     if config.release {
@@ -494,7 +506,7 @@ pub fn gen_page(config: &CrateConfig, serve: bool, skip_assets: 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");
     }
     if !skip_assets {
         let manifest = config.asset_manifest();

+ 62 - 6
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};
 
@@ -35,7 +36,8 @@ impl Autoformat {
         }
 
         if let Some(raw) = self.raw {
-            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
+            let indent = indentation_for(".")?;
+            if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
                 println!("{}", inner);
             } else {
                 // exit process with error
@@ -46,17 +48,21 @@ impl Autoformat {
 
         // Format single file
         if let Some(file) = self.file {
-            let file_content = if file == "-" {
+            let file_content;
+            let indent;
+            if file == "-" {
+                indent = indentation_for(".")?;
                 let mut contents = String::new();
                 std::io::stdin().read_to_string(&mut contents)?;
-                Ok(contents)
+                file_content = Ok(contents);
             } else {
-                fs::read_to_string(&file)
+                indent = indentation_for(".")?;
+                file_content = fs::read_to_string(&file);
             };
 
             match file_content {
                 Ok(s) => {
-                    let edits = dioxus_autofmt::fmt_file(&s);
+                    let edits = dioxus_autofmt::fmt_file(&s, indent);
                     let out = dioxus_autofmt::apply_formats(&s, edits);
                     if file == "-" {
                         print!("{}", out);
@@ -93,6 +99,12 @@ async fn autoformat_project(check: bool) -> Result<()> {
     let mut files_to_format = vec![];
     collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
 
+    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| {
@@ -104,10 +116,11 @@ async fn autoformat_project(check: bool) -> Result<()> {
         })
         .map(|path| async {
             let _path = path.clone();
+            let _indent = indent.clone();
             let res = tokio::spawn(async move {
                 let contents = tokio::fs::read_to_string(&path).await?;
 
-                let edits = dioxus_autofmt::fmt_file(&contents);
+                let edits = dioxus_autofmt::fmt_file(&contents, _indent.clone());
                 let len = edits.len();
 
                 if !edits.is_empty() {
@@ -151,6 +164,49 @@ async fn autoformat_project(check: bool) -> Result<()> {
     Ok(())
 }
 
+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: &Path, files: &mut Vec<PathBuf>) {
     let Ok(folder) = folder.read_dir() else {
         return;

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

@@ -39,8 +39,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 => {
@@ -98,8 +98,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

@@ -107,7 +107,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();

+ 30 - 41
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]
@@ -55,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
 
     let bin = get_bin(args.bin)?;
 
-    let _dioxus_config = DioxusConfig::load(bin.clone())
+    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");
@@ -72,15 +61,15 @@ async fn main() -> anyhow::Result<()> {
             .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
 
         Build(opts) => opts
-            .build(bin.clone())
+            .build(Some(bin.clone()))
             .map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
 
         Clean(opts) => opts
-            .clean(bin.clone())
+            .clean(Some(bin.clone()))
             .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
 
         Serve(opts) => opts
-            .serve(bin.clone())
+            .serve(Some(bin.clone()))
             .await
             .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
 
@@ -93,7 +82,7 @@ async fn main() -> anyhow::Result<()> {
             .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
 
         Bundle(opts) => opts
-            .bundle(bin.clone())
+            .bundle(Some(bin.clone()))
             .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
 
         #[cfg(feature = "plugin")]

+ 13 - 11
packages/cli/src/server/desktop/mod.rs

@@ -51,8 +51,6 @@ pub(crate) async fn startup_with_platform<P: Platform + Send + 'static>(
 
             let hot_reload_tx = broadcast::channel(100).0;
 
-            clear_paths();
-
             Some(HotReloadState {
                 messages: hot_reload_tx.clone(),
                 file_map: file_map.clone(),
@@ -103,7 +101,14 @@ async fn serve<P: Platform + Send + 'static>(
 }
 
 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
@@ -115,9 +120,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
@@ -175,17 +180,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);
         }
     }
 }

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

@@ -33,7 +33,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>| {
@@ -56,6 +56,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);
@@ -122,12 +132,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

@@ -60,6 +60,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

@@ -55,7 +55,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() {
@@ -78,11 +78,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 {
@@ -101,8 +102,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());
@@ -165,6 +167,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;
@@ -214,6 +217,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,
@@ -327,6 +332,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/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 - 0
packages/fermi/src/callback.rs

@@ -28,6 +28,7 @@ impl CallbackApi {
     }
 }
 
+#[must_use]
 pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
     todo!()
 }

+ 1 - 0
packages/fermi/src/hooks/atom_ref.rs

@@ -13,6 +13,7 @@ use std::{
 ///
 ///
 ///
+#[must_use]
 pub fn use_atom_ref<'a, T: 'static>(
     cx: &'a ScopeState,
     atom: &'static AtomRef<T>,

+ 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?"),
     })
 }

+ 2 - 0
packages/fermi/src/hooks/read.rs

@@ -2,10 +2,12 @@ use crate::{use_atom_root, AtomId, AtomRoot, Readable};
 use dioxus_core::{ScopeId, ScopeState};
 use std::rc::Rc;
 
+#[must_use]
 pub fn use_read<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &V {
     use_read_rc(cx, f).as_ref()
 }
 
+#[must_use]
 pub fn use_read_rc<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &Rc<V> {
     let root = use_atom_root(cx);
 

+ 1 - 0
packages/fermi/src/hooks/set.rs

@@ -2,6 +2,7 @@ use crate::{use_atom_root, Writable};
 use dioxus_core::ScopeState;
 use std::rc::Rc;
 
+#[must_use]
 pub fn use_set<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &Rc<dyn Fn(T)> {
     let root = use_atom_root(cx);
     cx.use_hook(|| {

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

@@ -30,6 +30,7 @@ use std::{
 ///     ))
 /// }
 /// ```
+#[must_use]
 pub fn use_atom_state<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &AtomState<T> {
     let root = crate::use_atom_root(cx);
 
@@ -85,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 {

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác