"));
+ }
props_docs.push(parse_quote! {
#[doc = #arg_doc]
From 85839abe5668a34c87a2d325ef2dd5a115ec7978 Mon Sep 17 00:00:00 2001
From: Leonard
Date: Tue, 24 Oct 2023 01:35:59 +0200
Subject: [PATCH 09/86] Fix random clippy warning
---
packages/desktop/src/protocol.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs
index 53652f82f..3d51e3ac1 100644
--- a/packages/desktop/src/protocol.rs
+++ b/packages/desktop/src/protocol.rs
@@ -153,7 +153,7 @@ fn get_asset_root() -> Option {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
- if trimmed.ends_with(".svg") {
+ if trimmed.extension().is_some_and(|ext| ext == ".svg") {
return Ok("image/svg+xml");
}
From 613f2f9acaff5295395c7ce001cb41561203a6b6 Mon Sep 17 00:00:00 2001
From: Leonard
Date: Tue, 24 Oct 2023 01:41:46 +0200
Subject: [PATCH 10/86] Revert "Fix random clippy warning"
This reverts commit 85839abe5668a34c87a2d325ef2dd5a115ec7978.
---
packages/desktop/src/protocol.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs
index 3d51e3ac1..53652f82f 100644
--- a/packages/desktop/src/protocol.rs
+++ b/packages/desktop/src/protocol.rs
@@ -153,7 +153,7 @@ fn get_asset_root() -> Option {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
- if trimmed.extension().is_some_and(|ext| ext == ".svg") {
+ if trimmed.ends_with(".svg") {
return Ok("image/svg+xml");
}
From 6e85ecea128d5d30aaadc6ae3f466623950b8c75 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Wed, 25 Oct 2023 10:23:23 -0500
Subject: [PATCH 11/86] fix use shared state lint in release mode
---
packages/hooks/src/use_shared_state.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs
index 37226c759..0c0f43508 100644
--- a/packages/hooks/src/use_shared_state.rs
+++ b/packages/hooks/src/use_shared_state.rs
@@ -26,6 +26,7 @@ macro_rules! debug_location {
}
pub mod error {
+ #[cfg(debug_assertions)]
fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String {
locations
.iter()
From b9dae3e1e0c535225a64756e58dd7570caabf9ad Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Wed, 25 Oct 2023 15:06:08 -0500
Subject: [PATCH 12/86] don't ignore leaks in miri
---
.github/workflows/miri.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml
index 4a467b9b2..f9ad3855f 100644
--- a/.github/workflows/miri.yml
+++ b/.github/workflows/miri.yml
@@ -86,8 +86,7 @@ jobs:
# working-directory: tokio
env:
- # todo: disable memory leaks ignore
- MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
+ MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
PROPTEST_CASES: 10
# Cache the global cargo directory, but NOT the local `target` directory which
From b25fada776c61013217540c0a3cd6f4294563c81 Mon Sep 17 00:00:00 2001
From: Stephen Andary
Date: Wed, 25 Oct 2023 16:36:38 -0400
Subject: [PATCH 13/86] Add Newtype so that Rust-url can be IntoRoutable
(#1579)
* add newtype so that rust-url can IntoRoutable
* add doc line
* implement From directly
---------
Co-authored-by: Evan Almloff
---
packages/router/src/components/link.rs | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs
index 787f9cdc9..639d99ae7 100644
--- a/packages/router/src/components/link.rs
+++ b/packages/router/src/components/link.rs
@@ -11,6 +11,8 @@ use crate::navigation::NavigationTarget;
use crate::prelude::Routable;
use crate::utils::use_router_internal::use_router_internal;
+use url::Url;
+
/// Something that can be converted into a [`NavigationTarget`].
#[derive(Clone)]
pub enum IntoRoutable {
@@ -53,6 +55,18 @@ impl From<&str> for IntoRoutable {
}
}
+impl From for IntoRoutable {
+ fn from(url: Url) -> Self {
+ IntoRoutable::FromStr(url.to_string())
+ }
+}
+
+impl From<&Url> for IntoRoutable {
+ fn from(url: &Url) -> Self {
+ IntoRoutable::FromStr(url.to_string())
+ }
+}
+
/// The properties for a [`Link`].
#[derive(Props)]
pub struct LinkProps<'a> {
From b99f081c0839defc9d9c61cb428f5e6b1464aab6 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Wed, 25 Oct 2023 16:37:04 -0500
Subject: [PATCH 14/86] drop any attribute after rendering
---
packages/core/src/arena.rs | 5 +----
packages/core/src/bump_frame.rs | 28 +++++++++++++++++++++++++---
packages/core/src/scope_arena.rs | 4 ++--
packages/core/src/scopes.rs | 12 +++++++++---
4 files changed, 37 insertions(+), 12 deletions(-)
diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs
index b7f768755..4f441bb81 100644
--- a/packages/core/src/arena.rs
+++ b/packages/core/src/arena.rs
@@ -164,16 +164,13 @@ 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();
- }
_ => (),
}
});
diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs
index 0fe7b3867..d4b3d2e07 100644
--- a/packages/core/src/bump_frame.rs
+++ b/packages/core/src/bump_frame.rs
@@ -1,10 +1,13 @@
use crate::nodes::RenderReturn;
+use crate::{Attribute, AttributeValue};
use bumpalo::Bump;
+use std::cell::RefCell;
use std::cell::{Cell, UnsafeCell};
pub(crate) struct BumpFrame {
pub bump: UnsafeCell,
pub node: Cell<*const RenderReturn<'static>>,
+ pub(crate) attributes_to_drop_before_reset: RefCell>>,
}
impl BumpFrame {
@@ -13,6 +16,7 @@ impl BumpFrame {
Self {
bump: UnsafeCell::new(bump),
node: Cell::new(std::ptr::null()),
+ attributes_to_drop_before_reset: Default::default(),
}
}
@@ -31,8 +35,26 @@ 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);
+ }
+
+ 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 };
+ match &attribute.value {
+ AttributeValue::Any(l) => {
+ _ = l.take();
+ }
+ _ => (),
+ }
+ });
+ unsafe {
+ let bump = &mut *self.bump.get();
+ bump.reset();
+ }
}
}
diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs
index 1ed5b816c..397f5328e 100644
--- a/packages/core/src/scope_arena.rs
+++ b/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);
diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs
index 996529a0d..3fd2aac30 100644
--- a/packages/core/src/scopes.rs
+++ b/packages/core/src/scopes.rs
@@ -94,7 +94,7 @@ pub struct ScopeState {
pub(crate) hook_idx: Cell,
pub(crate) borrowed_props: RefCell>>,
- pub(crate) attributes_to_drop: RefCell>>,
+ pub(crate) attributes_to_drop_before_render: RefCell>>,
pub(crate) props: Option>>,
}
@@ -348,13 +348,19 @@ 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);
+ }
_ => (),
}
From 370c6cb9d2333b5bee1f5bafe92d9dad05d8f11f Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Wed, 25 Oct 2023 16:47:04 -0500
Subject: [PATCH 15/86] fix clippy
---
packages/core/src/arena.rs | 7 ++-----
packages/core/src/bump_frame.rs | 7 ++-----
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs
index 4f441bb81..785d4b174 100644
--- a/packages/core/src/arena.rs
+++ b/packages/core/src/arena.rs
@@ -167,11 +167,8 @@ impl VirtualDom {
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();
- }
- _ => (),
+ if let AttributeValue::Listener(l) = &listener.value {
+ _ = l.take();
}
});
}
diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs
index d4b3d2e07..bf630c605 100644
--- a/packages/core/src/bump_frame.rs
+++ b/packages/core/src/bump_frame.rs
@@ -45,11 +45,8 @@ impl BumpFrame {
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
attributes.drain(..).for_each(|attribute| {
let attribute = unsafe { &*attribute };
- match &attribute.value {
- AttributeValue::Any(l) => {
- _ = l.take();
- }
- _ => (),
+ if let AttributeValue::Any(l) = &attribute.value {
+ _ = l.take();
}
});
unsafe {
From eea4a02ef59c07fcd2e143e838a4080a8d59cb49 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 26 Oct 2023 13:23:12 -0500
Subject: [PATCH 16/86] add comments about the cause of the leak
---
packages/core/src/bump_frame.rs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs
index 9ae11ab79..6927bd8bc 100644
--- a/packages/core/src/bump_frame.rs
+++ b/packages/core/src/bump_frame.rs
@@ -7,6 +7,8 @@ use std::cell::{Cell, UnsafeCell};
pub(crate) struct BumpFrame {
pub bump: UnsafeCell,
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>>,
pub(crate) props_to_drop_before_reset: RefCell>>,
}
@@ -43,6 +45,10 @@ impl BumpFrame {
.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| {
From f5bc1a9856c0287f14526830df6c83d98f1116e8 Mon Sep 17 00:00:00 2001
From: Emil Boman
Date: Thu, 26 Oct 2023 21:19:51 +0200
Subject: [PATCH 17/86] Implement LiveView Router Integration (#1505)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix GoForwardButton calling can_go_back instead of can_go_forward
* Add first draft of LiveviewHistory
* Add external URL redirect
* Lock evaluator channel outside loop
* Add liveview to router examples
* fixup! Add liveview to router examples
* Communicate with liveview server on page load
* Add PopState event to Liveview routing
* Call updater callback from liveview history
* Add rudimentary PopState functionality to liveview router.
* Fix linter errors
* Refactor
* Fix navigator external redirection not working.
* Add go back and go forward buttons to router examples
* Finish functionality for timeline stack in liveview router
* Add docs to LiveviewHistory
* Replace Liveview history context attachment with constructor that takes context
* Fix go forward/backward history/future shuffle
* Support history across entire liveview session, if contiguous page jumps.
* Remove unnecessary bounds
* Add query and hash to location string
* Run rustfmt
* fix: Update server function docs link (#1489)
* liveview: Add `interpreter_glue_relative_uri (#1481)
* liveview: Add `interpreter_glue_relative_uri`
By utilizing `window.location.host` in the client-side JavaScript, we can easily derive the WebSocket URI from a relative path URI. This approach obviates the need for host address retrieval on the server side, unlike the method of serving glue code in liveview using `interpreter_glue`.
* liveview: Merge `.._relative_url` functionality
- Merged `.._relative_url` to current API `interpreter_glue`.
- Edit axum example to work with new feature.
* liveview: Fix clippy warning
* Rename modules to use snake_case (#1498)
* Change Scope into &ScopeState
* Move synchronization of state into router and make it opt-in
---------
Co-authored-by: Marc EspÃn
Co-authored-by: Seungwoo Kang
Co-authored-by: Leonard
Co-authored-by: Evan Almloff
---
packages/router/Cargo.toml | 5 +
packages/router/examples/simple_routes.rs | 72 ++-
.../router/src/components/history_buttons.rs | 2 +-
packages/router/src/contexts/router.rs | 2 +-
packages/router/src/history/liveview.rs | 441 ++++++++++++++++++
packages/router/src/history/mod.rs | 5 +
6 files changed, 517 insertions(+), 10 deletions(-)
create mode 100644 packages/router/src/history/liveview.rs
diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml
index 80923d539..da9d3fe85 100644
--- a/packages/router/Cargo.toml
+++ b/packages/router/Cargo.toml
@@ -19,6 +19,7 @@ thiserror = { workspace = true }
futures-util = { workspace = true }
urlencoding = "2.1.3"
serde = { version = "1", features = ["derive"], optional = true }
+serde_json = { version = "1.0.91", optional = true }
url = "2.3.1"
wasm-bindgen = { workspace = true, optional = true }
web-sys = { version = "0.3.60", optional = true, features = [
@@ -26,18 +27,22 @@ web-sys = { version = "0.3.60", optional = true, features = [
] }
js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true }
+dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
[features]
default = ["web"]
ssr = ["dioxus-ssr", "tokio"]
+liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"]
wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
[dev-dependencies]
+axum = { version = "0.6.1", features = ["ws"] }
dioxus = { path = "../dioxus" }
+dioxus-liveview = { workspace = true, features = ["axum"] }
dioxus-ssr = { path = "../ssr" }
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs
index 3634f7df3..aac1e8658 100644
--- a/packages/router/examples/simple_routes.rs
+++ b/packages/router/examples/simple_routes.rs
@@ -2,6 +2,47 @@ use dioxus::prelude::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
+#[cfg(feature = "liveview")]
+#[tokio::main]
+async fn main() {
+ use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
+
+ let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
+ let view = dioxus_liveview::LiveViewPool::new();
+ let app = Router::new()
+ .fallback(get(move || async move {
+ Html(format!(
+ r#"
+
+
+
+
+ {glue}
+
+ "#,
+ glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
+ ))
+ }))
+ .route(
+ "/ws",
+ get(move |ws: WebSocketUpgrade| async move {
+ ws.on_upgrade(move |socket| async move {
+ _ = view
+ .launch(dioxus_liveview::axum_socket(socket), Root)
+ .await;
+ })
+ }),
+ );
+
+ println!("Listening on http://{listen_address}");
+
+ axum::Server::bind(&listen_address.to_string().parse().unwrap())
+ .serve(app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "liveview"))]
fn main() {
#[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch(Root);
@@ -10,21 +51,26 @@ fn main() {
dioxus_web::launch(root);
}
+#[cfg(feature = "liveview")]
#[component]
fn Root(cx: Scope) -> Element {
- render! {
- Router:: {}
- }
+ let history = LiveviewHistory::new(cx);
+ render! { Router:: {
+ config: || RouterConfig::default().history(history),
+ } }
+}
+
+#[cfg(not(feature = "liveview"))]
+#[component]
+fn Root(cx: Scope) -> Element {
+ render! { Router:: {} }
}
#[component]
fn UserFrame(cx: Scope, user_id: usize) -> Element {
render! {
- pre {
- "UserFrame{{\n\tuser_id:{user_id}\n}}"
- }
- div {
- background_color: "rgba(0,0,0,50%)",
+ pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
+ div { background_color: "rgba(0,0,0,50%)",
"children:"
Outlet:: {}
}
@@ -88,6 +134,16 @@ fn Route3(cx: Scope, dynamic: String) -> Element {
to: Route::Route2 { user_id: 8888 },
"hello world link"
}
+ button {
+ disabled: !navigator.can_go_back(),
+ onclick: move |_| { navigator.go_back(); },
+ "go back"
+ }
+ button {
+ disabled: !navigator.can_go_forward(),
+ onclick: move |_| { navigator.go_forward(); },
+ "go forward"
+ }
button {
onclick: move |_| { navigator.push("https://www.google.com"); },
"google link"
diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs
index eb62e9285..ae11ad276 100644
--- a/packages/router/src/components/history_buttons.rs
+++ b/packages/router/src/components/history_buttons.rs
@@ -142,7 +142,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
}
};
- let disabled = !router.can_go_back();
+ let disabled = !router.can_go_forward();
render! {
button {
diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs
index b15956991..079640e61 100644
--- a/packages/router/src/contexts/router.rs
+++ b/packages/router/src/contexts/router.rs
@@ -232,7 +232,7 @@ impl RouterContext {
IntoRoutable::FromStr(url) => {
let parsed_route: NavigationTarget> = match self.route_from_str(&url) {
Ok(route) => NavigationTarget::Internal(route),
- Err(err) => NavigationTarget::External(err),
+ Err(_) => NavigationTarget::External(url),
};
parsed_route
}
diff --git a/packages/router/src/history/liveview.rs b/packages/router/src/history/liveview.rs
new file mode 100644
index 000000000..9ffb09c34
--- /dev/null
+++ b/packages/router/src/history/liveview.rs
@@ -0,0 +1,441 @@
+use super::HistoryProvider;
+use crate::routable::Routable;
+use dioxus::prelude::*;
+use serde::{Deserialize, Serialize};
+use std::sync::{Mutex, RwLock};
+use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
+
+/// A [`HistoryProvider`] that evaluates history through JS.
+pub struct LiveviewHistory
+where
+ ::Err: std::fmt::Display,
+{
+ action_tx: tokio::sync::mpsc::UnboundedSender>,
+ timeline: Arc>>,
+ updater_callback: Arc>>,
+}
+
+struct Timeline
+where
+ ::Err: std::fmt::Display,
+{
+ current_index: usize,
+ routes: BTreeMap,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct State {
+ index: usize,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct Session
+where
+ ::Err: std::fmt::Display,
+{
+ #[serde(with = "routes")]
+ routes: BTreeMap,
+ last_visited: usize,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SessionStorage {
+ liveview: Option,
+}
+
+enum Action {
+ GoBack,
+ GoForward,
+ Push(R),
+ Replace(R),
+ External(String),
+}
+
+impl Timeline
+where
+ ::Err: std::fmt::Display,
+{
+ fn new(initial_path: R) -> Self {
+ Self {
+ current_index: 0,
+ routes: BTreeMap::from([(0, initial_path)]),
+ }
+ }
+
+ fn init(
+ &mut self,
+ route: R,
+ state: Option,
+ session: Option>,
+ depth: usize,
+ ) -> State {
+ if let Some(session) = session {
+ self.routes = session.routes;
+ if state.is_none() {
+ // top of stack
+ let last_visited = session.last_visited;
+ self.routes.retain(|&lhs, _| lhs <= last_visited);
+ }
+ };
+ let state = match state {
+ Some(state) => {
+ self.current_index = state.index;
+ state
+ }
+ None => {
+ let index = depth - 1;
+ self.current_index = index;
+ State { index }
+ }
+ };
+ self.routes.insert(state.index, route);
+ state
+ }
+
+ fn update(&mut self, route: R, state: Option) -> State {
+ if let Some(state) = state {
+ self.current_index = state.index;
+ self.routes.insert(self.current_index, route);
+ state
+ } else {
+ self.push(route)
+ }
+ }
+
+ fn push(&mut self, route: R) -> State {
+ // top of stack
+ let index = self.current_index + 1;
+ self.current_index = index;
+ self.routes.insert(index, route);
+ self.routes.retain(|&rhs, _| index >= rhs);
+ State {
+ index: self.current_index,
+ }
+ }
+
+ fn replace(&mut self, route: R) -> State {
+ self.routes.insert(self.current_index, route);
+ State {
+ index: self.current_index,
+ }
+ }
+
+ fn current_route(&self) -> &R {
+ &self.routes[&self.current_index]
+ }
+
+ fn session(&self) -> Session {
+ Session {
+ routes: self.routes.clone(),
+ last_visited: self.current_index,
+ }
+ }
+}
+
+impl LiveviewHistory
+where
+ ::Err: std::fmt::Display,
+{
+ /// Create a [`LiveviewHistory`] in the given scope.
+ /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
+ ///
+ /// # Panics
+ ///
+ /// Panics if not in a Liveview context.
+ pub fn new(cx: &ScopeState) -> Self {
+ Self::new_with_initial_path(
+ cx,
+ "/".parse().unwrap_or_else(|err| {
+ panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err)
+ }),
+ )
+ }
+
+ /// Create a [`LiveviewHistory`] in the given scope, starting at `initial_path`.
+ /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
+ ///
+ /// # Panics
+ ///
+ /// Panics if not in a Liveview context.
+ pub fn new_with_initial_path(cx: &ScopeState, initial_path: R) -> Self {
+ let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::>();
+ let action_rx = Arc::new(Mutex::new(action_rx));
+ let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
+ let updater_callback: Arc>> =
+ Arc::new(RwLock::new(Arc::new(|| {})));
+
+ let eval_provider = cx
+ .consume_context::>()
+ .expect("evaluator not provided");
+
+ let create_eval = Rc::new(move |script: &str| {
+ eval_provider
+ .new_evaluator(script.to_string())
+ .map(UseEval::new)
+ }) as Rc Result>;
+
+ // Listen to server actions
+ cx.push_future({
+ let timeline = timeline.clone();
+ let action_rx = action_rx.clone();
+ let create_eval = create_eval.clone();
+ async move {
+ let mut action_rx = action_rx.lock().expect("unpoisoned mutex");
+ loop {
+ let eval = action_rx.recv().await.expect("sender to exist");
+ let _ = match eval {
+ Action::GoBack => create_eval(
+ r#"
+ // this triggers a PopState event
+ history.back();
+ "#,
+ ),
+ Action::GoForward => create_eval(
+ r#"
+ // this triggers a PopState event
+ history.forward();
+ "#,
+ ),
+ Action::Push(route) => {
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.push(route.clone());
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+ create_eval(&format!(
+ r#"
+ // this does not trigger a PopState event
+ history.pushState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+ "#
+ ))
+ }
+ Action::Replace(route) => {
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.replace(route.clone());
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+ create_eval(&format!(
+ r#"
+ // this does not trigger a PopState event
+ history.replaceState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+ "#
+ ))
+ }
+ Action::External(url) => create_eval(&format!(
+ r#"
+ location.href = "{url}";
+ "#
+ )),
+ };
+ }
+ }
+ });
+
+ // Listen to browser actions
+ cx.push_future({
+ let updater = updater_callback.clone();
+ let timeline = timeline.clone();
+ let create_eval = create_eval.clone();
+ async move {
+ let popstate_eval = {
+ let init_eval = create_eval(
+ r#"
+ return [
+ document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
+ history.state,
+ JSON.parse(sessionStorage.getItem("liveview")),
+ history.length,
+ ];
+ "#,
+ ).expect("failed to load state").await.expect("serializable state");
+ let (route, state, session, depth) = serde_json::from_value::<(
+ String,
+ Option,
+ Option>,
+ usize,
+ )>(init_eval).expect("serializable state");
+ let Ok(route) = R::from_str(&route.to_string()) else {
+ return;
+ };
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.init(route.clone(), state, session, depth);
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+
+ // Call the updater callback
+ (updater.read().unwrap())();
+
+ create_eval(&format!(r#"
+ // this does not trigger a PopState event
+ history.replaceState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+
+ window.addEventListener("popstate", (event) => {{
+ dioxus.send([
+ document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
+ event.state,
+ ]);
+ }});
+ "#)).expect("failed to initialize popstate")
+ };
+
+ loop {
+ let event = match popstate_eval.recv().await {
+ Ok(event) => event,
+ Err(_) => continue,
+ };
+ let (route, state) = serde_json::from_value::<(String, Option)>(event).expect("serializable state");
+ let Ok(route) = R::from_str(&route.to_string()) else {
+ return;
+ };
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.update(route.clone(), state);
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+
+ let _ = create_eval(&format!(
+ r#"
+ // this does not trigger a PopState event
+ history.replaceState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+ "#));
+
+ // Call the updater callback
+ (updater.read().unwrap())();
+ }
+ }
+ });
+
+ Self {
+ action_tx,
+ timeline,
+ updater_callback,
+ }
+ }
+}
+
+impl HistoryProvider for LiveviewHistory
+where
+ ::Err: std::fmt::Display,
+{
+ fn go_back(&mut self) {
+ let _ = self.action_tx.send(Action::GoBack);
+ }
+
+ fn go_forward(&mut self) {
+ let _ = self.action_tx.send(Action::GoForward);
+ }
+
+ fn push(&mut self, route: R) {
+ let _ = self.action_tx.send(Action::Push(route));
+ }
+
+ fn replace(&mut self, route: R) {
+ let _ = self.action_tx.send(Action::Replace(route));
+ }
+
+ fn external(&mut self, url: String) -> bool {
+ let _ = self.action_tx.send(Action::External(url));
+ true
+ }
+
+ fn current_route(&self) -> R {
+ let timeline = self.timeline.lock().expect("unpoisoned mutex");
+ timeline.current_route().clone()
+ }
+
+ fn can_go_back(&self) -> bool {
+ let timeline = self.timeline.lock().expect("unpoisoned mutex");
+ // Check if the one before is contiguous (i.e., not an external page)
+ let visited_indices: Vec = timeline.routes.keys().cloned().collect();
+ visited_indices
+ .iter()
+ .position(|&rhs| timeline.current_index == rhs)
+ .map_or(false, |index| {
+ index > 0 && visited_indices[index - 1] == timeline.current_index - 1
+ })
+ }
+
+ fn can_go_forward(&self) -> bool {
+ let timeline = self.timeline.lock().expect("unpoisoned mutex");
+ // Check if the one after is contiguous (i.e., not an external page)
+ let visited_indices: Vec = timeline.routes.keys().cloned().collect();
+ visited_indices
+ .iter()
+ .rposition(|&rhs| timeline.current_index == rhs)
+ .map_or(false, |index| {
+ index < visited_indices.len() - 1
+ && visited_indices[index + 1] == timeline.current_index + 1
+ })
+ }
+
+ fn updater(&mut self, callback: Arc) {
+ let mut updater_callback = self.updater_callback.write().unwrap();
+ *updater_callback = callback;
+ }
+}
+
+mod routes {
+ use crate::prelude::Routable;
+ use core::str::FromStr;
+ use serde::de::{MapAccess, Visitor};
+ use serde::{ser::SerializeMap, Deserializer, Serializer};
+ use std::collections::BTreeMap;
+
+ pub fn serialize(routes: &BTreeMap, serializer: S) -> Result
+ where
+ S: Serializer,
+ R: Routable,
+ {
+ let mut map = serializer.serialize_map(Some(routes.len()))?;
+ for (index, route) in routes.iter() {
+ map.serialize_entry(&index.to_string(), &route.to_string())?;
+ }
+ map.end()
+ }
+
+ pub fn deserialize<'de, D, R>(deserializer: D) -> Result, D::Error>
+ where
+ D: Deserializer<'de>,
+ R: Routable,
+ ::Err: std::fmt::Display,
+ {
+ struct BTreeMapVisitor {
+ marker: std::marker::PhantomData,
+ }
+
+ impl<'de, R> Visitor<'de> for BTreeMapVisitor
+ where
+ R: Routable,
+ ::Err: std::fmt::Display,
+ {
+ type Value = BTreeMap;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_str("a map with indices and routable values")
+ }
+
+ fn visit_map(self, mut map: M) -> Result
+ where
+ M: MapAccess<'de>,
+ {
+ let mut routes = BTreeMap::new();
+ while let Some((index, route)) = map.next_entry::()? {
+ let index = index.parse::().map_err(serde::de::Error::custom)?;
+ let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
+ routes.insert(index, route);
+ }
+ Ok(routes)
+ }
+ }
+
+ deserializer.deserialize_map(BTreeMapVisitor {
+ marker: std::marker::PhantomData,
+ })
+ }
+}
diff --git a/packages/router/src/history/mod.rs b/packages/router/src/history/mod.rs
index e9668b979..cf7183366 100644
--- a/packages/router/src/history/mod.rs
+++ b/packages/router/src/history/mod.rs
@@ -22,6 +22,11 @@ pub use web::*;
#[cfg(feature = "web")]
pub(crate) mod web_history;
+#[cfg(feature = "liveview")]
+mod liveview;
+#[cfg(feature = "liveview")]
+pub use liveview::*;
+
// #[cfg(feature = "web")]
// mod web_hash;
// #[cfg(feature = "web")]
From d4e6876ff648159c78ad008ade4e3b1b469bb996 Mon Sep 17 00:00:00 2001
From: Falco Hirschenberger
Date: Fri, 27 Oct 2023 15:29:43 +0200
Subject: [PATCH 18/86] Silence desktop build outputs from cargo (#1584)
Silence additional disturbing output from cargo in desktop builds
---
packages/cli/src/builder.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs
index 68c494c42..c85cdc484 100644
--- a/packages/cli/src/builder.rs
+++ b/packages/cli/src/builder.rs
@@ -254,6 +254,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result
Date: Sun, 29 Oct 2023 21:39:07 +0330
Subject: [PATCH 19/86] Make window invisible until the first render (#1588)
---
packages/desktop/src/lib.rs | 7 +++++++
packages/desktop/src/webview.rs | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs
index badbe3276..bfefaf547 100644
--- a/packages/desktop/src/lib.rs
+++ b/packages/desktop/src/lib.rs
@@ -161,6 +161,7 @@ pub fn launch_with_props(root: Component
, props: P, cfg: Config)
// iOS panics if we create a window before the event loop is started
let props = Rc::new(Cell::new(Some(props)));
let cfg = Rc::new(Cell::new(Some(cfg)));
+ let mut is_visible_before_start = true;
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
@@ -210,6 +211,8 @@ pub fn launch_with_props(root: Component
, props: P, cfg: Config)
// Create a dom
let dom = VirtualDom::new_with_props(root, props);
+ is_visible_before_start = cfg.window.window.visible;
+
let handler = create_new_window(
cfg,
event_loop,
@@ -323,6 +326,10 @@ pub fn launch_with_props(root: Component
, 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" => {
diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs
index 469aa8068..6e6d26b17 100644
--- a/packages/desktop/src/webview.rs
+++ b/packages/desktop/src/webview.rs
@@ -13,7 +13,7 @@ pub fn build(
proxy: EventLoopProxy,
) -> (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();
From c5e647e97d0567e8362662a80ce474b95aa638ac Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Mon, 30 Oct 2023 08:39:37 -0500
Subject: [PATCH 20/86] add read untracked to signals
---
packages/signals/src/signal.rs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs
index 9213307ff..0ad96349d 100644
--- a/packages/signals/src/signal.rs
+++ b/packages/signals/src/signal.rs
@@ -200,6 +200,13 @@ impl Signal {
Ref::map(inner, |v| &v.value)
}
+ /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
+ /// If the signal has been dropped, this will panic.
+ pub fn read_untracked(&self) -> Ref {
+ let inner = self.inner.read();
+ Ref::map(inner, |v| &v.value)
+ }
+
/// Get a mutable reference to the signal's value.
/// If the signal has been dropped, this will panic.
pub fn write(&self) -> Write<'_, T> {
@@ -385,6 +392,12 @@ impl ReadOnlySignal {
self.inner.read()
}
+ /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
+ /// If the signal has been dropped, this will panic.
+ pub fn read_untracked(&self) -> Ref {
+ self.inner.read_untracked()
+ }
+
/// Run a closure with a reference to the signal's value.
pub fn with(&self, f: impl FnOnce(&T) -> O) -> O {
self.inner.with(f)
From 78120ca0e260c9986612b254da7f4afc95edbe97 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 30 Oct 2023 08:44:28 -0500
Subject: [PATCH 21/86] Bump actions/setup-node from 3 to 4 (#1592)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/setup-node
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/playwright.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 500d41564..cb5a1f1bb 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -20,7 +20,7 @@ jobs:
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: 16
- name: Install Rust
From 51f342417a856dc0dfea3701d1488953879f2d73 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Mon, 30 Oct 2023 14:41:41 -0500
Subject: [PATCH 22/86] add examples to the default watch path
---
packages/cli/src/assets/dioxus.toml | 2 +-
packages/cli/src/config.rs | 2 +-
packages/cli/src/server/mod.rs | 14 +++++++-------
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/packages/cli/src/assets/dioxus.toml b/packages/cli/src/assets/dioxus.toml
index 6386fb76e..892a6cdf8 100644
--- a/packages/cli/src/assets/dioxus.toml
+++ b/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]
diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs
index 0654dc375..3abf47f0c 100644
--- a/packages/cli/src/config.rs
+++ b/packages/cli/src/config.rs
@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
},
proxy: Some(vec![]),
watcher: WebWatcherConfig {
- watch_path: Some(vec![PathBuf::from("src")]),
+ watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
reload_html: Some(false),
index_on_404: Some(true),
},
diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs
index c6073d787..79a5531b6 100644
--- a/packages/cli/src/server/mod.rs
+++ b/packages/cli/src/server/mod.rs
@@ -32,7 +32,7 @@ async fn setup_file_watcher Result + 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| {
@@ -121,12 +121,12 @@ async fn setup_file_watcher Result + 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)
}
From bf36fc6def522c75f0f608d5c6a779ca1477f1ec Mon Sep 17 00:00:00 2001
From: tigerros
Date: Wed, 1 Nov 2023 14:14:07 +0100
Subject: [PATCH 23/86] Fix `#[component]` expansion Clippy warning (#1599)
Adds a `#[allow(clippy::inline_always)]` attribute to the generated `__dx_inner_comp` function.
---
.../core-macro/src/component_body_deserializers/component.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/core-macro/src/component_body_deserializers/component.rs b/packages/core-macro/src/component_body_deserializers/component.rs
index e43b1819f..36cda850c 100644
--- a/packages/core-macro/src/component_body_deserializers/component.rs
+++ b/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)
From fdec47b90bc09945ccb5f26012b492f38587d872 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Wed, 1 Nov 2023 08:19:35 -0500
Subject: [PATCH 24/86] fix clippy
---
packages/generational-box/src/lib.rs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs
index 628308340..f56904ede 100644
--- a/packages/generational-box/src/lib.rs
+++ b/packages/generational-box/src/lib.rs
@@ -485,7 +485,7 @@ pub struct GenerationalRef {
borrow: GenerationalRefBorrowInfo,
}
-impl<'a, T: 'static> GenerationalRef {
+impl GenerationalRef {
/// Map one ref type to another.
pub fn map(orig: GenerationalRef, f: F) -> GenerationalRef
where
@@ -522,7 +522,7 @@ impl<'a, T: 'static> GenerationalRef {
}
}
-impl<'a, T: 'static> Deref for GenerationalRef {
+impl Deref for GenerationalRef {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -586,7 +586,7 @@ impl GenerationalRefMut {
}
}
-impl<'a, T: 'static> Deref for GenerationalRefMut {
+impl Deref for GenerationalRefMut {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -594,7 +594,7 @@ impl<'a, T: 'static> Deref for GenerationalRefMut {
}
}
-impl<'a, T: 'static> DerefMut for GenerationalRefMut {
+impl DerefMut for GenerationalRefMut {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner.deref_mut()
}
From 52fb0801933eed40bea1c9edb58544693810e544 Mon Sep 17 00:00:00 2001
From: Alex Parrill
Date: Tue, 31 Oct 2023 20:48:19 -0400
Subject: [PATCH 25/86] Use indentation settings in dx fmt and vscode extension
Adds the ability to specify an indent string to public autofmt methods - either
a sequence of spaces or a tab character.
Get the indentation style and size from rustfmt for dx fmt, or from the editor
settings for the vscode extension.
Closes #1595
---
packages/autofmt/src/buffer.rs | 11 +-
packages/autofmt/src/element.rs | 12 +-
packages/autofmt/src/expr.rs | 6 +-
packages/autofmt/src/indent.rs | 108 ++++++++++++++++++
packages/autofmt/src/lib.rs | 27 ++---
packages/autofmt/src/writer.rs | 4 +-
packages/autofmt/tests/samples.rs | 2 +-
packages/autofmt/tests/wrong.rs | 15 ++-
.../wrong/{comments.rsx => comments-4sp.rsx} | 0
...ments.wrong.rsx => comments-4sp.wrong.rsx} | 0
packages/autofmt/tests/wrong/comments-tab.rsx | 7 ++
.../tests/wrong/comments-tab.wrong.rsx | 5 +
.../tests/wrong/{multi.rsx => multi-4sp.rsx} | 0
.../{multi.wrong.rsx => multi-4sp.wrong.rsx} | 0
packages/autofmt/tests/wrong/multi-tab.rsx | 3 +
.../autofmt/tests/wrong/multi-tab.wrong.rsx | 5 +
.../{multiexpr.rsx => multiexpr-4sp.rsx} | 0
...expr.wrong.rsx => multiexpr-4sp.wrong.rsx} | 0
.../autofmt/tests/wrong/multiexpr-tab.rsx | 8 ++
.../tests/wrong/multiexpr-tab.wrong.rsx | 5 +
packages/cli/src/cli/autoformat.rs | 68 ++++++++++-
packages/extension/src/lib.rs | 46 ++++++--
packages/extension/src/main.ts | 2 +-
23 files changed, 283 insertions(+), 51 deletions(-)
create mode 100644 packages/autofmt/src/indent.rs
rename packages/autofmt/tests/wrong/{comments.rsx => comments-4sp.rsx} (100%)
rename packages/autofmt/tests/wrong/{comments.wrong.rsx => comments-4sp.wrong.rsx} (100%)
create mode 100644 packages/autofmt/tests/wrong/comments-tab.rsx
create mode 100644 packages/autofmt/tests/wrong/comments-tab.wrong.rsx
rename packages/autofmt/tests/wrong/{multi.rsx => multi-4sp.rsx} (100%)
rename packages/autofmt/tests/wrong/{multi.wrong.rsx => multi-4sp.wrong.rsx} (100%)
create mode 100644 packages/autofmt/tests/wrong/multi-tab.rsx
create mode 100644 packages/autofmt/tests/wrong/multi-tab.wrong.rsx
rename packages/autofmt/tests/wrong/{multiexpr.rsx => multiexpr-4sp.rsx} (100%)
rename packages/autofmt/tests/wrong/{multiexpr.wrong.rsx => multiexpr-4sp.wrong.rsx} (100%)
create mode 100644 packages/autofmt/tests/wrong/multiexpr-tab.rsx
create mode 100644 packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs
index fea5a4a3f..f19a7d8dc 100644
--- a/packages/autofmt/src/buffer.rs
+++ b/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(())
}
diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs
index dbc336401..27e7b31f1 100644
--- a/packages/autofmt/src/element.rs
+++ b/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, " ")?
}
diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs
index b0257f8c3..b7a93a4fc 100644
--- a/packages/autofmt/src/expr.rs
+++ b/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();
diff --git a/packages/autofmt/src/indent.rs b/packages/autofmt/src/indent.rs
new file mode 100644
index 000000000..2cce7cf1e
--- /dev/null
+++ b/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
+ );
+ }
+}
diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs
index 64a342885..0c3fe6bb8 100644
--- a/packages/autofmt/src/lib.rs
+++ b/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 {
+pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec {
let mut formatted_blocks = Vec::new();
let parsed = syn::parse_file(contents).unwrap();
@@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec {
}
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 {
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 {
buf.consume()
}
-pub fn fmt_block(block: &str, indent_level: usize) -> Option {
+pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option {
let body = syn::parse_str::(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()
-}
diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs
index 59f135592..bb333e8df 100644
--- a/packages/autofmt/src/writer.rs
+++ b/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(())
}
diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs
index 8145b0e8e..9431d700e 100644
--- a/packages/autofmt/tests/samples.rs
+++ b/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", "");
diff --git a/packages/autofmt/tests/wrong.rs b/packages/autofmt/tests/wrong.rs
index 5a0fb8a87..145d03807 100644
--- a/packages/autofmt/tests/wrong.rs
+++ b/packages/autofmt/tests/wrong.rs
@@ -1,10 +1,12 @@
+use dioxus_autofmt::{IndentType, IndentOptions};
+
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)));
diff --git a/packages/autofmt/tests/wrong/comments.rsx b/packages/autofmt/tests/wrong/comments-4sp.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/comments.rsx
rename to packages/autofmt/tests/wrong/comments-4sp.rsx
diff --git a/packages/autofmt/tests/wrong/comments.wrong.rsx b/packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/comments.wrong.rsx
rename to packages/autofmt/tests/wrong/comments-4sp.wrong.rsx
diff --git a/packages/autofmt/tests/wrong/comments-tab.rsx b/packages/autofmt/tests/wrong/comments-tab.rsx
new file mode 100644
index 000000000..4c05e347e
--- /dev/null
+++ b/packages/autofmt/tests/wrong/comments-tab.rsx
@@ -0,0 +1,7 @@
+rsx! {
+ div {
+ // Comments
+ class: "asdasd",
+ "hello world"
+ }
+}
diff --git a/packages/autofmt/tests/wrong/comments-tab.wrong.rsx b/packages/autofmt/tests/wrong/comments-tab.wrong.rsx
new file mode 100644
index 000000000..eac96642d
--- /dev/null
+++ b/packages/autofmt/tests/wrong/comments-tab.wrong.rsx
@@ -0,0 +1,5 @@
+rsx! {
+ div {
+ // Comments
+ class: "asdasd", "hello world" }
+}
diff --git a/packages/autofmt/tests/wrong/multi.rsx b/packages/autofmt/tests/wrong/multi-4sp.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multi.rsx
rename to packages/autofmt/tests/wrong/multi-4sp.rsx
diff --git a/packages/autofmt/tests/wrong/multi.wrong.rsx b/packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multi.wrong.rsx
rename to packages/autofmt/tests/wrong/multi-4sp.wrong.rsx
diff --git a/packages/autofmt/tests/wrong/multi-tab.rsx b/packages/autofmt/tests/wrong/multi-tab.rsx
new file mode 100644
index 000000000..1fb85c3d0
--- /dev/null
+++ b/packages/autofmt/tests/wrong/multi-tab.rsx
@@ -0,0 +1,3 @@
+fn app(cx: Scope) -> Element {
+ cx.render(rsx! { div { "hello world" } })
+}
diff --git a/packages/autofmt/tests/wrong/multi-tab.wrong.rsx b/packages/autofmt/tests/wrong/multi-tab.wrong.rsx
new file mode 100644
index 000000000..d818f9535
--- /dev/null
+++ b/packages/autofmt/tests/wrong/multi-tab.wrong.rsx
@@ -0,0 +1,5 @@
+fn app(cx: Scope) -> Element {
+ cx.render(rsx! {
+ div {"hello world" }
+ })
+}
diff --git a/packages/autofmt/tests/wrong/multiexpr.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multiexpr.rsx
rename to packages/autofmt/tests/wrong/multiexpr-4sp.rsx
diff --git a/packages/autofmt/tests/wrong/multiexpr.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
similarity index 100%
rename from packages/autofmt/tests/wrong/multiexpr.wrong.rsx
rename to packages/autofmt/tests/wrong/multiexpr-4sp.wrong.rsx
diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.rsx
new file mode 100644
index 000000000..1fc3401c4
--- /dev/null
+++ b/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
+ }
+ })
+}
diff --git a/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
new file mode 100644
index 000000000..073107541
--- /dev/null
+++ b/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 }
+ })
+}
diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs
index 1d1cbcd24..6c1ce04a1 100644
--- a/packages/cli/src/cli/autoformat.rs
+++ b/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) -> Result {
+ 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::())
+ .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) {
let Ok(folder) = folder.read_dir() else {
return;
diff --git a/packages/extension/src/lib.rs b/packages/extension/src/lib.rs
index abcdaf598..fb9cca8a9 100644
--- a/packages/extension/src/lib.rs
+++ b/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 }
}
diff --git a/packages/extension/src/main.ts b/packages/extension/src/main.ts
index fc12457ad..4ceee487c 100644
--- a/packages/extension/src/main.ts
+++ b/packages/extension/src/main.ts
@@ -90,7 +90,7 @@ 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);
+ const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, editor.options.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
From 0f2923a385c94e13f53ba6920a632ed21d9fcc8d Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 2 Nov 2023 13:17:21 -0500
Subject: [PATCH 26/86] fix extension types
---
packages/extension/src/main.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/packages/extension/src/main.ts b/packages/extension/src/main.ts
index 4ceee487c..76b7261f1 100644
--- a/packages/extension/src/main.ts
+++ b/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, !editor.options.insertSpaces, editor.options.tabSize);
+ 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
From 0aa4875ec0b32e85f8d2d4e23997dacb9ddec3b6 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 2 Nov 2023 13:59:46 -0500
Subject: [PATCH 27/86] fix formating
---
packages/autofmt/tests/wrong.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/autofmt/tests/wrong.rs b/packages/autofmt/tests/wrong.rs
index 145d03807..06a0f00d1 100644
--- a/packages/autofmt/tests/wrong.rs
+++ b/packages/autofmt/tests/wrong.rs
@@ -1,4 +1,4 @@
-use dioxus_autofmt::{IndentType, IndentOptions};
+use dioxus_autofmt::{IndentOptions, IndentType};
macro_rules! twoway {
($val:literal => $name:ident ($indent:expr)) => {
From 7590cf822d892c360ed24b0825a5db034584e2e6 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 2 Nov 2023 21:00:43 -0500
Subject: [PATCH 28/86] fix the scroll event on the web renderer
---
packages/web/src/dom.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs
index 4fa5c9444..f45d6558a 100644
--- a/packages/web/src/dom.rs
+++ b/packages/web/src/dom.rs
@@ -294,7 +294,7 @@ pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -
"select" => Rc::new(SelectionData {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)),
- "scroll" => Rc::new(()),
+ "scroll" => Rc::new(ScrollData {}),
"wheel" => Rc::new(WheelData::from(event)),
"animationstart" | "animationend" | "animationiteration" => {
Rc::new(AnimationData::from(event))
From bb5738a0baf9f43af3db98a9a6fb481687026d86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc=20Esp=C3=ADn?=
Date: Sat, 4 Nov 2023 19:22:47 +0100
Subject: [PATCH 29/86] fix: Read value from root when calling
`AtomState::current()` (#1609)
---
packages/fermi/src/hooks/state.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs
index d4ebc529c..7d473588a 100644
--- a/packages/fermi/src/hooks/state.rs
+++ b/packages/fermi/src/hooks/state.rs
@@ -86,7 +86,9 @@ impl AtomState {
/// ```
#[must_use]
pub fn current(&self) -> Rc {
- 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.
From be1decf9f2ffc0b66cb7a176e9aafcabc2a529b2 Mon Sep 17 00:00:00 2001
From: Lee TaeWoo
Date: Sun, 5 Nov 2023 03:53:50 +0900
Subject: [PATCH 30/86] fix syntax error at derive `Props` using const generics
(#1607)
---
packages/core-macro/src/props/mod.rs | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index 605983afc..aba8137e7 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -551,18 +551,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),
From c18f91142937937a32708d9f9c9294cf249e9d76 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc=20Esp=C3=ADn?=
Date: Sat, 4 Nov 2023 20:04:14 +0100
Subject: [PATCH 31/86] Update atom_root.rs (#1611)
---
packages/fermi/src/hooks/atom_root.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs
index 24fd70093..468f5f980 100644
--- a/packages/fermi/src/hooks/atom_root.rs
+++ b/packages/fermi/src/hooks/atom_root.rs
@@ -7,6 +7,6 @@ use dioxus_core::ScopeState;
pub fn use_atom_root(cx: &ScopeState) -> &Rc {
cx.use_hook(|| match cx.consume_context::>() {
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?"),
})
}
From b9554fd744426eeb1fd2ae03202ec9c57f8d5d58 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Sun, 5 Nov 2023 08:32:39 -0600
Subject: [PATCH 32/86] add a noop evaluator to the ssr renderer
---
.../examples/axum-hello-world/src/main.rs | 1 +
packages/fullstack/src/render.rs | 2 +
packages/ssr/Cargo.toml | 3 ++
packages/ssr/src/eval.rs | 42 +++++++++++++++++++
packages/ssr/src/incremental.rs | 1 +
packages/ssr/src/lib.rs | 2 +
6 files changed, 51 insertions(+)
create mode 100644 packages/ssr/src/eval.rs
diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs
index 8f1d3c8c6..64e5b44a0 100644
--- a/packages/fullstack/examples/axum-hello-world/src/main.rs
+++ b/packages/fullstack/examples/axum-hello-world/src/main.rs
@@ -24,6 +24,7 @@ fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
+ let eval = use_eval(cx);
cx.render(rsx! {
div {
diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs
index cda41f82c..886a5bd99 100644
--- a/packages/fullstack/src/render.rs
+++ b/packages/fullstack/src/render.rs
@@ -45,6 +45,8 @@ impl SsrRendererPool {
.expect("couldn't spawn runtime")
.block_on(async move {
let mut vdom = VirtualDom::new_with_props(component, props);
+ // Make sure the evaluator is initialized
+ dioxus_ssr::eval::init_eval(vdom.base_scope());
let mut to = WriteBuffer { buffer: Vec::new() };
// before polling the future, we need to set the context
let prev_context =
diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml
index f6376452d..71f5b26a0 100644
--- a/packages/ssr/Cargo.toml
+++ b/packages/ssr/Cargo.toml
@@ -10,6 +10,7 @@ keywords = ["dom", "ui", "gui", "react", "ssr"]
[dependencies]
dioxus-core = { workspace = true, features = ["serialize"] }
+dioxus-html = { workspace = true }
askama_escape = "0.10.3"
thiserror = "1.0.23"
rustc-hash = "1.1.0"
@@ -17,6 +18,8 @@ lru = "0.10.0"
tracing = { workspace = true }
http = "0.2.9"
tokio = { version = "1.28", features = ["full"], optional = true }
+async-trait = "0.1.58"
+serde_json = { version = "1.0" }
[dev-dependencies]
dioxus = { workspace = true }
diff --git a/packages/ssr/src/eval.rs b/packages/ssr/src/eval.rs
new file mode 100644
index 000000000..2ab74f4e1
--- /dev/null
+++ b/packages/ssr/src/eval.rs
@@ -0,0 +1,42 @@
+use async_trait::async_trait;
+use dioxus_core::ScopeState;
+use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
+use std::rc::Rc;
+
+/// Provides the SSREvalProvider through [`cx.provide_context`].
+pub fn init_eval(cx: &ScopeState) {
+ let provider: Rc = Rc::new(SSREvalProvider {});
+ cx.provide_context(provider);
+}
+
+/// Reprents the ssr-target's provider of evaluators.
+pub struct SSREvalProvider;
+impl EvalProvider for SSREvalProvider {
+ fn new_evaluator(&self, _: String) -> Result, EvalError> {
+ Ok(Rc::new(SSREvaluator) as Rc)
+ }
+}
+
+/// Represents a ssr-target's JavaScript evaluator.
+pub struct SSREvaluator;
+
+// In ssr rendering we never run or resolve evals.
+#[async_trait(?Send)]
+impl Evaluator for SSREvaluator {
+ /// Runs the evaluated JavaScript.
+ async fn join(&self) -> Result {
+ std::future::pending::<()>().await;
+ unreachable!()
+ }
+
+ /// Sends a message to the evaluated JavaScript.
+ fn send(&self, _el: serde_json::Value) -> Result<(), EvalError> {
+ Ok(())
+ }
+
+ /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
+ async fn recv(&self) -> Result {
+ std::future::pending::<()>().await;
+ unreachable!()
+ }
+}
diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs
index c3bb25314..34ffa3443 100644
--- a/packages/ssr/src/incremental.rs
+++ b/packages/ssr/src/incremental.rs
@@ -81,6 +81,7 @@ impl IncrementalRenderer {
let mut html_buffer = WriteBuffer { buffer: Vec::new() };
{
let mut vdom = VirtualDom::new_with_props(comp, props);
+ crate::eval::init_eval(vdom.base_scope());
rebuild_with(&mut vdom).await;
renderer.render_before_body(&mut *html_buffer)?;
diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs
index 6794d1f14..fdc0e9333 100644
--- a/packages/ssr/src/lib.rs
+++ b/packages/ssr/src/lib.rs
@@ -10,6 +10,7 @@ pub mod incremental;
#[cfg(feature = "incremental")]
mod incremental_cfg;
+pub mod eval;
pub mod renderer;
pub mod template;
@@ -42,6 +43,7 @@ pub fn render_lazy(f: LazyNodes<'_, '_>) -> String {
};
let mut dom = VirtualDom::new_with_props(lazy_app, props);
+ crate::eval::init_eval(dom.base_scope());
_ = dom.rebuild();
Renderer::new().render(&dom)
From ca556ea9cbeef5217b05b84d467049daa9ae76fc Mon Sep 17 00:00:00 2001
From: xTeKc <81730792+xTeKc@users.noreply.github.com>
Date: Mon, 6 Nov 2023 14:32:42 -0500
Subject: [PATCH 33/86] update readme (#1615)
* Fix wasm-bindgen version mismatch
* fixes #1613
---
Cargo.toml | 2 +-
packages/cli/README.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index a78f60d3e..d687d74d0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -88,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 = [
diff --git a/packages/cli/README.md b/packages/cli/README.md
index 72a813468..8379f22cc 100644
--- a/packages/cli/README.md
+++ b/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
From 8ea7f076e5ff2b0a4ccc06cbfb93e93428d41b4b Mon Sep 17 00:00:00 2001
From: Joshua Wolfe <1369773+WolfeCub@users.noreply.github.com>
Date: Wed, 8 Nov 2023 09:33:41 -0500
Subject: [PATCH 34/86] Updated server_fn to 0.5.2 (#1620)
---
packages/fullstack/Cargo.toml | 2 +-
packages/fullstack/src/server_fn.rs | 8 --------
packages/server-macro/Cargo.toml | 2 +-
3 files changed, 2 insertions(+), 10 deletions(-)
diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml
index 8ab213e78..8de27a9b1 100644
--- a/packages/fullstack/Cargo.toml
+++ b/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
diff --git a/packages/fullstack/src/server_fn.rs b/packages/fullstack/src/server_fn.rs
index 539a0d70a..fd0639372 100644
--- a/packages/fullstack/src/server_fn.rs
+++ b/packages/fullstack/src/server_fn.rs
@@ -125,14 +125,6 @@ impl server_fn::ServerFunctionRegistry<()> for DioxusServerFnRegistry {
}
}
- fn register(
- url: &'static str,
- server_function: ServerFunction,
- encoding: server_fn::Encoding,
- ) -> Result<(), Self::Error> {
- Self::register_explicit("", url, server_function, encoding)
- }
-
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option> {
REGISTERED_SERVER_FUNCTIONS
diff --git a/packages/server-macro/Cargo.toml b/packages/server-macro/Cargo.toml
index 5a38acb40..a91cc626a 100644
--- a/packages/server-macro/Cargo.toml
+++ b/packages/server-macro/Cargo.toml
@@ -17,7 +17,7 @@ proc-macro2 = "^1.0.63"
quote = "^1.0.26"
syn = { version = "2", features = ["full"] }
convert_case = "^0.6.0"
-server_fn_macro = "^0.4.6"
+server_fn_macro = "^0.5.2"
[lib]
proc-macro = true
From d4b0451d096c67616be4b27de7a9e482ac5d8fd6 Mon Sep 17 00:00:00 2001
From: tigerros
Date: Thu, 9 Nov 2023 19:31:59 +0100
Subject: [PATCH 35/86] Allow warning in necessary functions (#1626)
---
packages/core-macro/src/props/mod.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index aba8137e7..6e6c43d66 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -631,7 +631,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,
@@ -823,6 +823,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;
@@ -841,6 +842,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
}
From 7f4e2af0c48e33915e96f0ac5e75b4c18e706632 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Mon, 13 Nov 2023 08:17:08 -0600
Subject: [PATCH 36/86] fix string memory leak
---
packages/core/src/nodes.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs
index c83b1059a..b6a685ad3 100644
--- a/packages/core/src/nodes.rs
+++ b/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(),
})
}
From 62719c309a26e5320b0bc9dde04a9d1689e351e4 Mon Sep 17 00:00:00 2001
From: Alexander
Date: Tue, 14 Nov 2023 16:39:37 +0000
Subject: [PATCH 37/86] fix keyboard input on calc example (#1639)
---
examples/calculator.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/examples/calculator.rs b/examples/calculator.rs
index bc7f13459..648ccfbc3 100644
--- a/examples/calculator.rs
+++ b/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",
From 098689083d9961640ac2da7f2a4c12c58a80ed07 Mon Sep 17 00:00:00 2001
From: Raman Hafiyatulin
Date: Wed, 15 Nov 2023 23:14:16 +0200
Subject: [PATCH 38/86] Related to #1547: use `dioxus-cli` within a workspace
(wildcard-members, real package names) (#1642)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Related to #1547: use `dioxus-cli` within a workspace
Although the `dx` CLI allows to specify a package name to chose from workspace members,
it does not support workspace members specified as glob-wildcards.
Neither it respects the effective package name, specified in the crate's `Cargo.toml`.
This PR addresses that issue:
- upon `dx build ...`, if the `--bin` CLI-argument is provided, treat the current dir as a workspace;
- search through the workspace's `members`: resolve each of them with `glob`;
- assume that any workspace member has a `Cargo.toml` in it (cargo does it, so it's okay);
- read said manifest, and check the package name in it;
- if found — there we have our sought package.
* Use cargo-metadata to find out the workspace structure
* glob is unused
---
packages/cli/src/error.rs | 3 ++
packages/cli/src/main.rs | 67 ++++++++++++++++-----------------------
2 files changed, 31 insertions(+), 39 deletions(-)
diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs
index 84b9d4b71..d577b2b40 100644
--- a/packages/cli/src/error.rs
+++ b/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),
diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs
index fe860a0e6..7ca1b2c9a 100644
--- a/packages/cli/src/main.rs
+++ b/packages/cli/src/main.rs
@@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
use Commands::*;
-fn get_bin(bin: Option) -> Result
> {
- const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
+fn get_bin(bin: Option) -> Result {
+ 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)?
+ };
- if let Some(ref bin) = bin {
- let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
- .map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
+ let crate_dir = package
+ .manifest_path
+ .parent()
+ .ok_or("couldn't take parent dir".into())
+ .map_err(Error::CargoError)?;
- 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)
+ 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")]
From 83f7ef9a447547c720e310b10e1682dc7762bdbc Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Sun, 19 Nov 2023 09:58:00 -0600
Subject: [PATCH 39/86] add an optional cleanup closure to the use effect hook
---
examples/readme.rs | 6 +++
packages/hooks/src/use_effect.rs | 79 ++++++++++++++++++++++++++++----
2 files changed, 76 insertions(+), 9 deletions(-)
diff --git a/examples/readme.rs b/examples/readme.rs
index f4668488b..b39c04765 100644
--- a/examples/readme.rs
+++ b/examples/readme.rs
@@ -11,6 +11,12 @@ fn main() {
fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
+ use_effect(cx, (), move |()| async {});
+
+ use_effect(cx, (count.get(),), move |(count,)| async move {
+ move || println!("Count unmounted from {}", count)
+ });
+
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
diff --git a/packages/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs
index e72afe27f..1f5faecdd 100644
--- a/packages/hooks/src/use_effect.rs
+++ b/packages/hooks/src/use_effect.rs
@@ -1,5 +1,10 @@
use dioxus_core::{ScopeState, TaskId};
-use std::{any::Any, cell::Cell, future::Future};
+use std::{
+ any::Any,
+ cell::{Cell, RefCell},
+ future::Future,
+ rc::Rc,
+};
use crate::UseFutureDep;
@@ -14,7 +19,7 @@ use crate::UseFutureDep;
/// ## Arguments
///
/// - `dependencies`: a tuple of references to values that are `PartialEq` + `Clone`.
-/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future.
+/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future. That future may return nothing or a closure that will be executed when the dependencies change to clean up the effect.
///
/// ## Examples
///
@@ -33,6 +38,16 @@ use crate::UseFutureDep;
/// }
/// });
///
+/// // Only fetch the user data when the id changes.
+/// use_effect(cx, (id,), |(id,)| {
+/// to_owned![name];
+/// async move {
+/// let user = fetch_user(id).await;
+/// name.set(user.name);
+/// move || println!("Cleaning up from {}", id);
+/// }
+/// });
+///
/// let name = name.get().clone().unwrap_or("Loading...".to_string());
///
/// render!(
@@ -45,34 +60,80 @@ use crate::UseFutureDep;
/// render!(Profile { id: 0 })
/// }
/// ```
-pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F)
+pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> R)
where
- T: 'static,
- F: Future