From df85b25548cfff642af2b4ac4a4521b53027cb52 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 21 Aug 2023 14:23:02 -0500 Subject: [PATCH 01/62] Add debug information to borrows and ownership in signals --- packages/generational-box/Cargo.toml | 1 + packages/generational-box/src/lib.rs | 445 ++++++++++++++++++++++++--- packages/signals/src/effect.rs | 2 +- packages/signals/src/impls.rs | 33 +- packages/signals/src/rt.rs | 33 +- packages/signals/src/signal.rs | 58 +++- 6 files changed, 497 insertions(+), 75 deletions(-) diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml index 6c5a5a841..cc0db9cec 100644 --- a/packages/generational-box/Cargo.toml +++ b/packages/generational-box/Cargo.toml @@ -15,3 +15,4 @@ rand = "0.8.5" [features] default = ["check_generation"] check_generation = [] +debug_borrows = [] diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index 60bd41419..7d64a1ed3 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -2,9 +2,13 @@ #![warn(missing_docs)] use std::{ + any::Any, cell::{Cell, Ref, RefCell, RefMut}, - fmt::Debug, + error::Error, + fmt::{Debug, Display}, marker::PhantomData, + ops::{Deref, DerefMut}, + panic::Location, rc::Rc, }; @@ -153,6 +157,8 @@ pub struct GenerationalBox { raw: MemoryLocation, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: u32, + #[cfg(any(debug_assertions, feature = "check_generation"))] + created_at: &'static std::panic::Location<'static>, _marker: PhantomData, } @@ -161,7 +167,7 @@ impl Debug for GenerationalBox { #[cfg(any(debug_assertions, feature = "check_generation"))] f.write_fmt(format_args!( "{:?}@{:?}", - self.raw.data.as_ptr(), + self.raw.0.data.as_ptr(), self.generation ))?; #[cfg(not(any(debug_assertions, feature = "check_generation")))] @@ -175,7 +181,7 @@ impl GenerationalBox { fn validate(&self) -> bool { #[cfg(any(debug_assertions, feature = "check_generation"))] { - self.raw.generation.get() == self.generation + self.raw.0.generation.get() == self.generation } #[cfg(not(any(debug_assertions, feature = "check_generation")))] { @@ -184,43 +190,51 @@ impl GenerationalBox { } /// Try to read the value. Returns None if the value is no longer valid. - pub fn try_read(&self) -> Option> { - self.validate() - .then(|| { - Ref::filter_map(self.raw.data.borrow(), |any| { - any.as_ref()?.downcast_ref::() - }) - .ok() - }) - .flatten() + #[track_caller] + pub fn try_read(&self) -> Result, BorrowError> { + if !self.validate() { + return Err(BorrowError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: self.created_at, + })); + } + self.raw.try_borrow( + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + self.created_at, + ) } /// Read the value. Panics if the value is no longer valid. - pub fn read(&self) -> Ref<'_, T> { + #[track_caller] + pub fn read(&self) -> GenerationalRef<'_, T> { self.try_read().unwrap() } /// Try to write the value. Returns None if the value is no longer valid. - pub fn try_write(&self) -> Option> { - self.validate() - .then(|| { - RefMut::filter_map(self.raw.data.borrow_mut(), |any| { - any.as_mut()?.downcast_mut::() - }) - .ok() - }) - .flatten() + #[track_caller] + pub fn try_write(&self) -> Result, BorrowMutError> { + if !self.validate() { + return Err(BorrowMutError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: self.created_at, + })); + } + self.raw.try_borrow_mut( + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + self.created_at, + ) } /// Write the value. Panics if the value is no longer valid. - pub fn write(&self) -> RefMut<'_, T> { + #[track_caller] + pub fn write(&self) -> GenerationalRefMut<'_, T> { self.try_write().unwrap() } /// Set the value. Panics if the value is no longer valid. pub fn set(&self, value: T) { self.validate().then(|| { - *self.raw.data.borrow_mut() = Some(Box::new(value)); + *self.raw.0.data.borrow_mut() = Some(Box::new(value)); }); } @@ -228,7 +242,8 @@ impl GenerationalBox { pub fn ptr_eq(&self, other: &Self) -> bool { #[cfg(any(debug_assertions, feature = "check_generation"))] { - self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation + self.raw.0.data.as_ptr() == other.raw.0.data.as_ptr() + && self.generation == other.generation } #[cfg(not(any(debug_assertions, feature = "check_generation")))] { @@ -246,26 +261,38 @@ impl Clone for GenerationalBox { } #[derive(Clone, Copy)] -struct MemoryLocation { - data: &'static RefCell>>, +struct MemoryLocation(&'static MemoryLocationInner); + +struct MemoryLocationInner { + data: RefCell>>, #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: &'static Cell, + generation: Cell, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: RefCell>>, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: Cell>>, } impl MemoryLocation { #[allow(unused)] fn drop(&self) { - let old = self.data.borrow_mut().take(); + let old = self.0.data.borrow_mut().take(); #[cfg(any(debug_assertions, feature = "check_generation"))] if old.is_some() { drop(old); - let new_generation = self.generation.get() + 1; - self.generation.set(new_generation); + let new_generation = self.0.generation.get() + 1; + self.0.generation.set(new_generation); } } - fn replace(&mut self, value: T) -> GenerationalBox { - let mut inner_mut = self.data.borrow_mut(); + fn replace_with_caller( + &mut self, + value: T, + #[cfg(any(debug_assertions, feature = "check_generation"))] caller: &'static Location< + 'static, + >, + ) -> GenerationalBox { + let mut inner_mut = self.0.data.borrow_mut(); let raw = Box::new(value); let old = inner_mut.replace(raw); @@ -273,10 +300,318 @@ impl MemoryLocation { GenerationalBox { raw: *self, #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: self.generation.get(), + generation: self.0.generation.get(), + #[cfg(any(debug_assertions, feature = "check_generation"))] + created_at: caller, _marker: PhantomData, } } + + #[track_caller] + fn try_borrow( + &self, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: &'static std::panic::Location<'static>, + ) -> Result, BorrowError> { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + self.0 + .borrowed_at + .borrow_mut() + .push(std::panic::Location::caller()); + match self.0.data.try_borrow() { + Ok(borrow) => match Ref::filter_map(borrow, |any| any.as_ref()?.downcast_ref::()) { + Ok(reference) => Ok(GenerationalRef { + inner: reference, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo { + borrowed_at: std::panic::Location::caller(), + borrowed_from: self.0, + }, + }), + Err(_) => Err(BorrowError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at, + })), + }, + Err(_) => Err(BorrowError::AlreadyBorrowedMut(AlreadyBorrowedMutError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: self.0.borrowed_mut_at.get().unwrap(), + })), + } + } + + #[track_caller] + fn try_borrow_mut( + &self, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: &'static std::panic::Location<'static>, + ) -> Result, BorrowMutError> { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + { + self.0 + .borrowed_mut_at + .set(Some(std::panic::Location::caller())); + } + match self.0.data.try_borrow_mut() { + Ok(borrow_mut) => { + match RefMut::filter_map(borrow_mut, |any| any.as_mut()?.downcast_mut::()) { + Ok(reference) => Ok(GenerationalRefMut { + inner: reference, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefMutBorrowInfo { + borrowed_from: self.0, + }, + }), + Err(_) => Err(BorrowMutError::Dropped(ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at, + })), + } + } + Err(_) => Err(BorrowMutError::AlreadyBorrowed(AlreadyBorrowedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: self.0.borrowed_at.borrow().clone(), + })), + } + } +} + +#[derive(Debug, Clone)] +/// An error that can occur when trying to borrow a value. +pub enum BorrowError { + /// The value was dropped. + Dropped(ValueDroppedError), + /// The value was already borrowed mutably. + AlreadyBorrowedMut(AlreadyBorrowedMutError), +} + +impl Display for BorrowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BorrowError::Dropped(error) => Display::fmt(error, f), + BorrowError::AlreadyBorrowedMut(error) => Display::fmt(error, f), + } + } +} + +impl Error for BorrowError {} + +#[derive(Debug, Clone)] +/// An error that can occur when trying to borrow a value mutably. +pub enum BorrowMutError { + /// The value was dropped. + Dropped(ValueDroppedError), + /// The value was already borrowed. + AlreadyBorrowed(AlreadyBorrowedError), + /// The value was already borrowed mutably. + AlreadyBorrowedMut(AlreadyBorrowedMutError), +} + +impl Display for BorrowMutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BorrowMutError::Dropped(error) => Display::fmt(error, f), + BorrowMutError::AlreadyBorrowedMut(error) => Display::fmt(error, f), + BorrowMutError::AlreadyBorrowed(error) => Display::fmt(error, f), + } + } +} + +impl Error for BorrowMutError {} + +/// An error that can occur when trying to use a value that has been dropped. +#[derive(Debug, Copy, Clone)] +pub struct ValueDroppedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + created_at: &'static std::panic::Location<'static>, +} + +impl Display for ValueDroppedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to borrow because the value was dropped.")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + f.write_fmt(format_args!("created_at: {}", self.created_at))?; + Ok(()) + } +} + +impl std::error::Error for ValueDroppedError {} + +/// An error that can occur when trying to borrow a value that has already been borrowed mutably. +#[derive(Debug, Copy, Clone)] +pub struct AlreadyBorrowedMutError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: &'static std::panic::Location<'static>, +} + +impl Display for AlreadyBorrowedMutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to borrow because the value was already borrowed mutably.")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + f.write_fmt(format_args!("borrowed_mut_at: {}", self.borrowed_mut_at))?; + Ok(()) + } +} + +impl std::error::Error for AlreadyBorrowedMutError {} + +/// An error that can occur when trying to borrow a value mutably that has already been borrowed immutably. +#[derive(Debug, Clone)] +pub struct AlreadyBorrowedError { + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: Vec<&'static std::panic::Location<'static>>, +} + +impl Display for AlreadyBorrowedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to borrow mutably because the value was already borrowed immutably.")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + f.write_str("borrowed_at:")?; + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + for location in self.borrowed_at.iter() { + f.write_fmt(format_args!("\t{}", location))?; + } + Ok(()) + } +} + +impl std::error::Error for AlreadyBorrowedError {} + +/// A reference to a value in a generational box. +pub struct GenerationalRef<'a, T: 'static> { + inner: Ref<'a, T>, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo, +} + +impl<'a, T: 'static> GenerationalRef<'a, T> { + /// Map one ref type to another. + pub fn map(orig: GenerationalRef<'a, T>, f: F) -> GenerationalRef<'a, U> + where + F: FnOnce(&T) -> &U, + { + GenerationalRef { + inner: Ref::map(orig.inner, f), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo { + borrowed_at: orig.borrow.borrowed_at, + borrowed_from: orig.borrow.borrowed_from, + }, + } + } + + /// Filter one ref type to another. + pub fn filter_map(orig: GenerationalRef<'a, T>, f: F) -> Option> + where + F: FnOnce(&T) -> Option<&U>, + { + let Self { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow, + } = orig; + Ref::filter_map(inner, f).ok().map(|inner| GenerationalRef { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefBorrowInfo { + borrowed_at: borrow.borrowed_at, + borrowed_from: borrow.borrowed_from, + }, + }) + } +} + +impl<'a, T: 'static> Deref for GenerationalRef<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +struct GenerationalRefBorrowInfo { + borrowed_at: &'static std::panic::Location<'static>, + borrowed_from: &'static MemoryLocationInner, +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +impl Drop for GenerationalRefBorrowInfo { + fn drop(&mut self) { + self.borrowed_from + .borrowed_at + .borrow_mut() + .retain(|location| std::ptr::eq(*location, self.borrowed_at as *const _)); + } +} + +/// A mutable reference to a value in a generational box. +pub struct GenerationalRefMut<'a, T: 'static> { + inner: RefMut<'a, T>, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: GenerationalRefMutBorrowInfo, +} + +impl<'a, T: 'static> GenerationalRefMut<'a, T> { + /// Map one ref type to another. + pub fn map(orig: GenerationalRefMut<'a, T>, f: F) -> GenerationalRefMut<'a, U> + where + F: FnOnce(&mut T) -> &mut U, + { + GenerationalRefMut { + inner: RefMut::map(orig.inner, f), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow: orig.borrow, + } + } + + /// Filter one ref type to another. + pub fn filter_map( + orig: GenerationalRefMut<'a, T>, + f: F, + ) -> Option> + where + F: FnOnce(&mut T) -> Option<&mut U>, + { + let Self { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow, + } = orig; + RefMut::filter_map(inner, f) + .ok() + .map(|inner| GenerationalRefMut { + inner, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrow, + }) + } +} + +impl<'a, T: 'static> Deref for GenerationalRefMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl<'a, T: 'static> DerefMut for GenerationalRefMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.deref_mut() + } +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +struct GenerationalRefMutBorrowInfo { + borrowed_from: &'static MemoryLocationInner, +} + +#[cfg(any(debug_assertions, feature = "debug_borrows"))] +impl Drop for GenerationalRefMutBorrowInfo { + fn drop(&mut self) { + self.borrowed_from.borrowed_mut_at.take(); + } } /// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread. @@ -305,12 +640,16 @@ impl Store { if let Some(location) = self.recycled.borrow_mut().pop() { location } else { - let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None)); - MemoryLocation { - data, + let data: &'static MemoryLocationInner = self.bump.alloc(MemoryLocationInner { + data: RefCell::new(None), #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: self.bump.alloc(Cell::new(0)), - } + generation: Cell::new(0), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_at: Default::default(), + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + borrowed_mut_at: Default::default(), + }); + MemoryLocation(data) } } @@ -331,9 +670,31 @@ pub struct Owner { impl Owner { /// Insert a value into the store. The value will be dropped when the owner is dropped. + #[track_caller] pub fn insert(&self, value: T) -> GenerationalBox { let mut location = self.store.claim(); - let key = location.replace(value); + let key = location.replace_with_caller( + value, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + std::panic::Location::caller(), + ); + self.owned.borrow_mut().push(location); + key + } + + /// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped. + pub fn insert_with_caller( + &self, + value: T, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + caller: &'static std::panic::Location<'static>, + ) -> GenerationalBox { + let mut location = self.store.claim(); + let key = location.replace_with_caller( + value, + #[cfg(any(debug_assertions, feature = "debug_borrows"))] + caller, + ); self.owned.borrow_mut().push(location); key } @@ -344,7 +705,9 @@ impl Owner { GenerationalBox { raw: location, #[cfg(any(debug_assertions, feature = "check_generation"))] - generation: location.generation.get(), + generation: location.0.generation.get(), + #[cfg(any(debug_assertions, feature = "check_generation"))] + created_at: std::panic::Location::caller(), _marker: PhantomData, } } diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs index 0685938a0..0110ce4d1 100644 --- a/packages/signals/src/effect.rs +++ b/packages/signals/src/effect.rs @@ -83,7 +83,7 @@ impl Effect { /// Run the effect callback immediately. Returns `true` if the effect was run. Returns `false` is the effect is dead. pub fn try_run(&self) { - if let Some(mut callback) = self.callback.try_write() { + if let Ok(mut callback) = self.callback.try_write() { { get_effect_stack().effects.borrow_mut().push(*self); } diff --git a/packages/signals/src/impls.rs b/packages/signals/src/impls.rs index 6427c2e2b..8029ac312 100644 --- a/packages/signals/src/impls.rs +++ b/packages/signals/src/impls.rs @@ -1,7 +1,7 @@ use crate::rt::CopyValue; use crate::signal::{ReadOnlySignal, Signal, Write}; - -use std::cell::{Ref, RefMut}; +use generational_box::GenerationalRef; +use generational_box::GenerationalRefMut; use std::{ fmt::{Debug, Display}, @@ -38,8 +38,8 @@ macro_rules! read_impls { impl $ty> { /// Read a value from the inner vector. - pub fn get(&self, index: usize) -> Option> { - Ref::filter_map(self.read(), |v| v.get(index)).ok() + pub fn get(&self, index: usize) -> Option> { + GenerationalRef::filter_map(self.read(), |v| v.get(index)) } } @@ -52,9 +52,9 @@ macro_rules! read_impls { self.with(|v| v.clone()).unwrap() } - /// Attemps to read the inner value of the Option. - pub fn as_ref(&self) -> Option> { - Ref::filter_map(self.read(), |v| v.as_ref()).ok() + /// Attempts to read the inner value of the Option. + pub fn as_ref(&self) -> Option> { + GenerationalRef::filter_map(self.read(), |v| v.as_ref()) } } }; @@ -182,19 +182,22 @@ macro_rules! write_impls { } /// Gets the value out of the Option, or inserts the given value if the Option is empty. - pub fn get_or_insert(&self, default: T) -> Ref<'_, T> { + pub fn get_or_insert(&self, default: T) -> GenerationalRef<'_, T> { self.get_or_insert_with(|| default) } /// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty. - pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> Ref<'_, T> { + pub fn get_or_insert_with( + &self, + default: impl FnOnce() -> T, + ) -> GenerationalRef<'_, T> { let borrow = self.read(); if borrow.is_none() { drop(borrow); self.with_mut(|v| *v = Some(default())); - Ref::map(self.read(), |v| v.as_ref().unwrap()) + GenerationalRef::map(self.read(), |v| v.as_ref().unwrap()) } else { - Ref::map(borrow, |v| v.as_ref().unwrap()) + GenerationalRef::map(borrow, |v| v.as_ref().unwrap()) } } } @@ -238,15 +241,15 @@ impl IntoIterator for CopyValue> { impl CopyValue> { /// Write to an element in the inner vector. - pub fn get_mut(&self, index: usize) -> Option> { - RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok() + pub fn get_mut(&self, index: usize) -> Option> { + GenerationalRefMut::filter_map(self.write(), |v| v.get_mut(index)) } } impl CopyValue> { /// Deref the inner value mutably. - pub fn as_mut(&self) -> Option> { - RefMut::filter_map(self.write(), |v| v.as_mut()).ok() + pub fn as_mut(&self) -> Option> { + GenerationalRefMut::filter_map(self.write(), |v| v.as_mut()) } } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index 7322d43b4..acf598217 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -1,14 +1,15 @@ -use std::cell::{Ref, RefMut}; - +use std::panic::Location; use std::rc::Rc; use dioxus_core::prelude::{ - consume_context, consume_context_from_scope, current_scope_id, provide_context, + consume_context, consume_context_from_scope, current_scope_id, has_context, provide_context, provide_context_to_scope, provide_root_context, }; use dioxus_core::ScopeId; -use generational_box::{GenerationalBox, Owner, Store}; +use generational_box::{ + BorrowError, BorrowMutError, GenerationalBox, GenerationalRef, GenerationalRefMut, Owner, Store, +}; fn current_store() -> Store { match consume_context() { @@ -21,7 +22,7 @@ fn current_store() -> Store { } fn current_owner() -> Rc { - match consume_context() { + match has_context() { Some(rt) => rt, None => { let owner = Rc::new(current_store().owner()); @@ -74,6 +75,7 @@ impl CopyValue { /// Create a new CopyValue. The value will be stored in the current component. /// /// Once the component this value is created in is dropped, the value will be dropped. + #[track_caller] pub fn new(value: T) -> Self { let owner = current_owner(); @@ -83,6 +85,15 @@ impl CopyValue { } } + pub(crate) fn new_with_caller(value: T, caller: &'static Location<'static>) -> Self { + let owner = current_owner(); + + Self { + value: owner.insert_with_caller(value, caller), + origin_scope: current_scope_id().expect("in a virtual dom"), + } + } + /// Create a new CopyValue. The value will be stored in the given scope. When the specified scope is dropped, the value will be dropped. pub fn new_in_scope(value: T, scope: ScopeId) -> Self { let owner = owner_in_scope(scope); @@ -108,22 +119,26 @@ impl CopyValue { } /// Try to read the value. If the value has been dropped, this will return None. - pub fn try_read(&self) -> Option> { + #[track_caller] + pub fn try_read(&self) -> Result, BorrowError> { self.value.try_read() } /// Read the value. If the value has been dropped, this will panic. - pub fn read(&self) -> Ref<'_, T> { + #[track_caller] + pub fn read(&self) -> GenerationalRef<'_, T> { self.value.read() } /// Try to write the value. If the value has been dropped, this will return None. - pub fn try_write(&self) -> Option> { + #[track_caller] + pub fn try_write(&self) -> Result, BorrowMutError> { self.value.try_write() } /// Write the value. If the value has been dropped, this will panic. - pub fn write(&self) -> RefMut<'_, T> { + #[track_caller] + pub fn write(&self) -> GenerationalRefMut<'_, T> { self.value.write() } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index a5599a1ce..1656f559e 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,6 +1,7 @@ use std::{ - cell::{Ref, RefCell, RefMut}, + cell::RefCell, ops::{Deref, DerefMut}, + panic::Location, rc::Rc, sync::Arc, }; @@ -9,6 +10,7 @@ use dioxus_core::{ prelude::{current_scope_id, has_context, provide_context, schedule_update_any}, ScopeId, ScopeState, }; +use generational_box::{GenerationalRef, GenerationalRefMut}; use crate::{CopyValue, Effect}; @@ -43,8 +45,18 @@ use crate::{CopyValue, Effect}; /// } /// } /// ``` +#[track_caller] pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { - *cx.use_hook(|| Signal::new(f())) + #[cfg(debug_assertions)] + let caller = Location::caller(); + + *cx.use_hook(|| { + Signal::new_with_caller( + f(), + #[cfg(debug_assertions)] + caller, + ) + }) } #[derive(Clone)] @@ -134,6 +146,7 @@ impl<'de, T: serde::Deserialize<'de> + 'static> serde::Deserialize<'de> for Sign impl Signal { /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. + #[track_caller] pub fn new(value: T) -> Self { Self { inner: CopyValue::new(SignalData { @@ -145,6 +158,25 @@ impl Signal { } } + /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. + fn new_with_caller( + value: T, + #[cfg(debug_assertions)] caller: &'static Location<'static>, + ) -> Self { + Self { + inner: CopyValue::new_with_caller( + SignalData { + subscribers: Default::default(), + effect_subscribers: Default::default(), + update_any: schedule_update_any().expect("in a virtual dom"), + value, + }, + #[cfg(debug_assertions)] + caller, + ), + } + } + /// Get the scope the signal was created in. pub fn origin_scope(&self) -> ScopeId { self.inner.origin_scope() @@ -152,7 +184,8 @@ impl Signal { /// Get the current value of the signal. This will subscribe the current scope to the signal. /// If the signal has been dropped, this will panic. - pub fn read(&self) -> Ref { + #[track_caller] + pub fn read(&self) -> GenerationalRef { let inner = self.inner.read(); if let Some(effect) = Effect::current() { let mut effect_subscribers = inner.effect_subscribers.borrow_mut(); @@ -176,14 +209,15 @@ impl Signal { } } } - Ref::map(inner, |v| &v.value) + GenerationalRef::map(inner, |v| &v.value) } /// Get a mutable reference to the signal's value. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn write(&self) -> Write<'_, T> { let inner = self.inner.write(); - let borrow = RefMut::map(inner, |v| &mut v.value); + let borrow = GenerationalRefMut::map(inner, |v| &mut v.value); Write { write: borrow, signal: SignalSubscriberDrop { signal: *self }, @@ -219,12 +253,14 @@ impl Signal { } /// Set the value of the signal. This will trigger an update on all subscribers. + #[track_caller] pub fn set(&self, value: T) { *self.write() = value; } /// Run a closure with a reference to the signal's value. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { let write = self.read(); f(&*write) @@ -232,6 +268,7 @@ impl Signal { /// Run a closure with a mutable reference to the signal's value. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn with_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { let mut write = self.write(); f(&mut *write) @@ -241,6 +278,7 @@ impl Signal { impl Signal { /// Get the current value of the signal. This will subscribe the current scope to the signal. /// If the signal has been dropped, this will panic. + #[track_caller] pub fn value(&self) -> T { self.read().clone() } @@ -264,7 +302,7 @@ impl Drop for SignalSubscriberDrop { /// A mutable reference to a signal's value. pub struct Write<'a, T: 'static, I: 'static = T> { - write: RefMut<'a, T>, + write: GenerationalRefMut<'a, T>, signal: SignalSubscriberDrop, } @@ -273,7 +311,7 @@ impl<'a, T: 'static, I: 'static> Write<'a, T, I> { pub fn map(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write<'a, O, I> { let Self { write, signal } = myself; Write { - write: RefMut::map(write, f), + write: GenerationalRefMut::map(write, f), signal, } } @@ -284,7 +322,7 @@ impl<'a, T: 'static, I: 'static> Write<'a, T, I> { f: impl FnOnce(&mut T) -> Option<&mut O>, ) -> Option> { let Self { write, signal } = myself; - let write = RefMut::filter_map(write, f).ok(); + let write = GenerationalRefMut::filter_map(write, f); write.map(|write| Write { write, signal }) } } @@ -320,11 +358,13 @@ impl ReadOnlySignal { } /// Get the current value of the signal. This will subscribe the current scope to the signal. - pub fn read(&self) -> Ref { + #[track_caller] + pub fn read(&self) -> GenerationalRef { self.inner.read() } /// Run a closure with a reference to the signal's value. + #[track_caller] pub fn with(&self, f: impl FnOnce(&T) -> O) -> O { self.inner.with(f) } From f09a2e2280f74a9ac4e50d298e6f665bad1f8ea2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 21 Aug 2023 14:28:45 -0500 Subject: [PATCH 02/62] fix release builds --- packages/generational-box/Cargo.toml | 1 + packages/generational-box/src/lib.rs | 26 ++++++++++++-------------- packages/signals/src/rt.rs | 12 +++++++++--- packages/signals/src/signal.rs | 5 ++--- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml index cc0db9cec..7151ec103 100644 --- a/packages/generational-box/Cargo.toml +++ b/packages/generational-box/Cargo.toml @@ -16,3 +16,4 @@ rand = "0.8.5" default = ["check_generation"] check_generation = [] debug_borrows = [] +debug_ownership = [] diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index 7d64a1ed3..1f8cbbe11 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -8,7 +8,6 @@ use std::{ fmt::{Debug, Display}, marker::PhantomData, ops::{Deref, DerefMut}, - panic::Location, rc::Rc, }; @@ -157,7 +156,7 @@ pub struct GenerationalBox { raw: MemoryLocation, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: u32, - #[cfg(any(debug_assertions, feature = "check_generation"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: &'static std::panic::Location<'static>, _marker: PhantomData, } @@ -288,9 +287,8 @@ impl MemoryLocation { fn replace_with_caller( &mut self, value: T, - #[cfg(any(debug_assertions, feature = "check_generation"))] caller: &'static Location< - 'static, - >, + #[cfg(any(debug_assertions, feature = "debug_ownership"))] + caller: &'static std::panic::Location<'static>, ) -> GenerationalBox { let mut inner_mut = self.0.data.borrow_mut(); @@ -301,7 +299,7 @@ impl MemoryLocation { raw: *self, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: self.0.generation.get(), - #[cfg(any(debug_assertions, feature = "check_generation"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: caller, _marker: PhantomData, } @@ -310,7 +308,7 @@ impl MemoryLocation { #[track_caller] fn try_borrow( &self, - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: &'static std::panic::Location<'static>, ) -> Result, BorrowError> { #[cfg(any(debug_assertions, feature = "debug_borrows"))] @@ -329,7 +327,7 @@ impl MemoryLocation { }, }), Err(_) => Err(BorrowError::Dropped(ValueDroppedError { - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at, })), }, @@ -343,7 +341,7 @@ impl MemoryLocation { #[track_caller] fn try_borrow_mut( &self, - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: &'static std::panic::Location<'static>, ) -> Result, BorrowMutError> { #[cfg(any(debug_assertions, feature = "debug_borrows"))] @@ -363,7 +361,7 @@ impl MemoryLocation { }, }), Err(_) => Err(BorrowMutError::Dropped(ValueDroppedError { - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at, })), } @@ -422,14 +420,14 @@ impl Error for BorrowMutError {} /// An error that can occur when trying to use a value that has been dropped. #[derive(Debug, Copy, Clone)] pub struct ValueDroppedError { - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: &'static std::panic::Location<'static>, } impl Display for ValueDroppedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Failed to borrow because the value was dropped.")?; - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] f.write_fmt(format_args!("created_at: {}", self.created_at))?; Ok(()) } @@ -686,7 +684,7 @@ impl Owner { pub fn insert_with_caller( &self, value: T, - #[cfg(any(debug_assertions, feature = "debug_borrows"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] caller: &'static std::panic::Location<'static>, ) -> GenerationalBox { let mut location = self.store.claim(); @@ -706,7 +704,7 @@ impl Owner { raw: location, #[cfg(any(debug_assertions, feature = "check_generation"))] generation: location.0.generation.get(), - #[cfg(any(debug_assertions, feature = "check_generation"))] + #[cfg(any(debug_assertions, feature = "debug_ownership"))] created_at: std::panic::Location::caller(), _marker: PhantomData, } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index acf598217..aee3b0858 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -1,4 +1,3 @@ -use std::panic::Location; use std::rc::Rc; use dioxus_core::prelude::{ @@ -85,11 +84,18 @@ impl CopyValue { } } - pub(crate) fn new_with_caller(value: T, caller: &'static Location<'static>) -> Self { + pub(crate) fn new_with_caller( + value: T, + #[cfg(debug_assertions)] caller: &'static std::panic::Location<'static>, + ) -> Self { let owner = current_owner(); Self { - value: owner.insert_with_caller(value, caller), + value: owner.insert_with_caller( + value, + #[cfg(debug_assertions)] + caller, + ), origin_scope: current_scope_id().expect("in a virtual dom"), } } diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 1656f559e..70a809e85 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,7 +1,6 @@ use std::{ cell::RefCell, ops::{Deref, DerefMut}, - panic::Location, rc::Rc, sync::Arc, }; @@ -48,7 +47,7 @@ use crate::{CopyValue, Effect}; #[track_caller] pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { #[cfg(debug_assertions)] - let caller = Location::caller(); + let caller = std::panic::Location::caller(); *cx.use_hook(|| { Signal::new_with_caller( @@ -161,7 +160,7 @@ impl Signal { /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. fn new_with_caller( value: T, - #[cfg(debug_assertions)] caller: &'static Location<'static>, + #[cfg(debug_assertions)] caller: &'static std::panic::Location<'static>, ) -> Self { Self { inner: CopyValue::new_with_caller( From 18f8daf0bdc30a6eb71ab7e91863442a13d47ca6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 21 Aug 2023 15:00:31 -0500 Subject: [PATCH 03/62] fix generational box tests --- packages/generational-box/README.md | 2 ++ packages/generational-box/src/lib.rs | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/generational-box/README.md b/packages/generational-box/README.md index 5ab7926ba..f8d4885b8 100644 --- a/packages/generational-box/README.md +++ b/packages/generational-box/README.md @@ -11,6 +11,8 @@ Three main types manage state in Generational Box: Example: ```rust +use generational_box::Store; + // Create a store for this thread let store = Store::default(); diff --git a/packages/generational-box/src/lib.rs b/packages/generational-box/src/lib.rs index 1f8cbbe11..84a81769d 100644 --- a/packages/generational-box/src/lib.rs +++ b/packages/generational-box/src/lib.rs @@ -32,12 +32,12 @@ fn reused() { let first_ptr; { let owner = store.owner(); - first_ptr = owner.insert(1).raw.data.as_ptr(); + first_ptr = owner.insert(1).raw.0.data.as_ptr(); drop(owner); } { let owner = store.owner(); - let second_ptr = owner.insert(1234).raw.data.as_ptr(); + let second_ptr = owner.insert(1234).raw.0.data.as_ptr(); assert_eq!(first_ptr, second_ptr); drop(owner); } @@ -56,7 +56,10 @@ fn leaking_is_ok() { // don't drop the owner std::mem::forget(owner); } - assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string())); + assert_eq!( + key.try_read().as_deref().unwrap(), + &"hello world".to_string() + ); } #[test] @@ -71,7 +74,7 @@ fn drops() { key = owner.insert(data); // drop the owner } - assert!(key.try_read().is_none()); + assert!(key.try_read().is_err()); } #[test] @@ -132,7 +135,7 @@ fn fuzz() { println!("{:?}", path); for key in valid_keys.iter() { let value = key.read(); - println!("{:?}", value); + println!("{:?}", &*value); assert!(value.starts_with("hello world")); } #[cfg(any(debug_assertions, feature = "check_generation"))] From d994e3e7221b404b1a69c6f099a9b46a9e5a4eed Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 23 Sep 2023 07:57:41 -0500 Subject: [PATCH 04/62] make the layer module public in fullstack --- packages/fullstack/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index f2d3e5533..384560c0a 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -19,7 +19,7 @@ mod hooks; mod hot_reload; pub mod launch; #[cfg(feature = "ssr")] -mod layer; +pub mod layer; #[cfg(feature = "ssr")] mod render; #[cfg(feature = "ssr")] From d2f0f4b4b6009f995c9dcb1e228cb8f67f93d4a9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 25 Sep 2023 12:25:21 -0500 Subject: [PATCH 05/62] document layer and service --- packages/fullstack/src/layer.rs | 5 +++++ packages/fullstack/src/lib.rs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/fullstack/src/layer.rs b/packages/fullstack/src/layer.rs index 10e67b3f5..60f76107b 100644 --- a/packages/fullstack/src/layer.rs +++ b/packages/fullstack/src/layer.rs @@ -3,7 +3,9 @@ use tracing_futures::Instrument; use http::{Request, Response}; +/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service pub trait Layer: Send + Sync + 'static { + /// Wrap a boxed service with this layer fn layer(&self, inner: BoxedService) -> BoxedService; } @@ -17,7 +19,9 @@ where } } +/// A service is a function that takes a request and returns an async response pub trait Service { + /// Run the service and produce a future that resolves to a response fn run( &mut self, req: http::Request, @@ -55,6 +59,7 @@ where } } +/// A boxed service is a type-erased service that can be used without knowing the underlying type pub struct BoxedService(pub Box); impl tower::Service> for BoxedService { diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 384560c0a..75707b931 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -19,7 +19,7 @@ mod hooks; mod hot_reload; pub mod launch; #[cfg(feature = "ssr")] -pub mod layer; +mod layer; #[cfg(feature = "ssr")] mod render; #[cfg(feature = "ssr")] @@ -40,6 +40,8 @@ pub mod prelude { #[cfg(not(feature = "ssr"))] pub use crate::html_storage::deserialize::get_root_props_from_document; pub use crate::launch::LaunchBuilder; + #[cfg(feature = "ssr")] + pub use crate::layer::{Layer, Service}; #[cfg(all(feature = "ssr", feature = "router"))] pub use crate::render::pre_cache_static_routes_with_props; #[cfg(feature = "ssr")] From 6e85ecea128d5d30aaadc6ae3f466623950b8c75 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 25 Oct 2023 10:23:23 -0500 Subject: [PATCH 06/62] 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 07/62] 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 b99f081c0839defc9d9c61cb428f5e6b1464aab6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 25 Oct 2023 16:37:04 -0500 Subject: [PATCH 08/62] 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 09/62] 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 10/62] 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 720bfdf23137f22b439b0f167f9d1d0466b6dcb1 Mon Sep 17 00:00:00 2001 From: HKalbasi <45197576+HKalbasi@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:39:07 +0330 Subject: [PATCH 11/62] 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 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 12/62] 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 13/62] 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 14/62] 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 15/62] 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 16/62] 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 17/62] 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 18/62] 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 19/62] 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 20/62] 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 21/62] 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 22/62] 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 23/62] 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 24/62] 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 25/62] 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 26/62] 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 27/62] 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 28/62] 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 29/62] 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 30/62] 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 + 'static, D: UseFutureDep, + R: UseEffectReturn, { struct UseEffect { needs_regen: bool, task: Cell>, dependencies: Vec>, + cleanup: UseEffectCleanup, + } + + impl Drop for UseEffect { + fn drop(&mut self) { + if let Some(cleanup) = self.cleanup.borrow_mut().take() { + cleanup(); + } + } } let state = cx.use_hook(move || UseEffect { needs_regen: true, task: Cell::new(None), dependencies: Vec::new(), + cleanup: Rc::new(RefCell::new(None)), }); if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen { + // Call the cleanup function if it exists + if let Some(cleanup) = state.cleanup.borrow_mut().take() { + cleanup(); + } + // We don't need regen anymore state.needs_regen = false; // Create the new future - let fut = future(dependencies.out()); + let return_value = future(dependencies.out()); - state.task.set(Some(cx.push_future(async move { - fut.await; - }))); + if let Some(task) = return_value.apply(state.cleanup.clone(), cx) { + state.task.set(Some(task)); + } + } +} + +type UseEffectCleanup = Rc>>>; + +/// Something that can be returned from a `use_effect` hook. +pub trait UseEffectReturn { + fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option; +} + +impl UseEffectReturn<()> for T +where + T: Future + 'static, +{ + fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> Option { + Some(cx.push_future(self)) + } +} + +#[doc(hidden)] +pub struct CleanupFutureMarker; +impl UseEffectReturn for T +where + T: Future + 'static, + F: FnOnce() + 'static, +{ + fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option { + let task = cx.push_future(async move { + let cleanup = self.await; + *oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box); + }); + Some(task) } } From 3c115bbef76f21435d8db886a799a47503026e5f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Nov 2023 10:03:18 -0600 Subject: [PATCH 31/62] add into attribute value impl for String --- packages/core/src/nodes.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c83b1059a..b489a7e4d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -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) From ff6c7efb41ef16bea467bfad164b33a2fb20219e Mon Sep 17 00:00:00 2001 From: "HJin.me" Date: Mon, 20 Nov 2023 01:22:54 +0800 Subject: [PATCH 32/62] fix: tailwind.css use absolute path (#1649) --- packages/cli/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index c85cdc484..71be8f115 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -469,7 +469,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { .unwrap_or_default() .contains_key("tailwindcss") { - style_str.push_str("\n"); + style_str.push_str("\n"); } replace_or_insert_before("{style_include}", &style_str, " Date: Mon, 20 Nov 2023 22:24:16 +0800 Subject: [PATCH 33/62] Fix grammar typo in comment (#1652) --- packages/fullstack/src/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fullstack/src/router.rs b/packages/fullstack/src/router.rs index b556aa000..b8d93f0e0 100644 --- a/packages/fullstack/src/router.rs +++ b/packages/fullstack/src/router.rs @@ -53,7 +53,7 @@ fn default_external_navigation_handler() -> fn(Scope) -> Element { dioxus_router::prelude::FailureExternalNavigation } -/// The configeration for the router +/// The configuration for the router #[derive(Props, serde::Serialize, serde::Deserialize)] pub struct FullstackRouterConfig where From 0ee21dff1e962bb5e1c22cfde15ecb4bc07c49fb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 20 Nov 2023 09:33:39 -0600 Subject: [PATCH 34/62] fix missing item in hot reloading diffing --- packages/cli/src/logging.rs | 14 +++++- packages/cli/src/server/output.rs | 25 ++++++----- packages/rsx/Cargo.toml | 1 + .../rsx/src/hot_reload/hot_reload_diff.rs | 44 ++++++++++++++++--- .../src/hot_reload/hot_reloading_file_map.rs | 27 +++++++++--- 5 files changed, 87 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 48c2a2788..31d088737 100644 --- a/packages/cli/src/logging.rs +++ b/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(); diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 71323dd29..4e148b0f1 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -22,17 +22,20 @@ pub fn print_console_info( options: PrettierOptions, web_info: Option, ) { - 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(); diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index bc866acb7..72b51a051 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -20,6 +20,7 @@ quote = { version = "1.0" } serde = { version = "1.0", features = ["derive"], optional = true } internment = { version = "0.7.0", optional = true } krates = { version = "0.12.6", optional = true } +tracing.workspace = true [features] hot_reload = ["krates", "internment"] diff --git a/packages/rsx/src/hot_reload/hot_reload_diff.rs b/packages/rsx/src/hot_reload/hot_reload_diff.rs index 22a01875d..7e19d3aad 100644 --- a/packages/rsx/src/hot_reload/hot_reload_diff.rs +++ b/packages/rsx/src/hot_reload/hot_reload_diff.rs @@ -1,4 +1,5 @@ use proc_macro2::TokenStream; +use quote::ToTokens; use syn::{File, Macro}; pub enum DiffResult { @@ -10,13 +11,30 @@ pub enum DiffResult { pub fn find_rsx(new: &File, old: &File) -> DiffResult { let mut rsx_calls = Vec::new(); if new.items.len() != old.items.len() { + tracing::trace!( + "found not hot reload-able change {:#?} != {:#?}", + new.items + .iter() + .map(|i| i.to_token_stream().to_string()) + .collect::>(), + old.items + .iter() + .map(|i| i.to_token_stream().to_string()) + .collect::>() + ); return DiffResult::CodeChanged; } for (new, old) in new.items.iter().zip(old.items.iter()) { if find_rsx_item(new, old, &mut rsx_calls) { + tracing::trace!( + "found not hot reload-able change {:#?} != {:#?}", + new.to_token_stream().to_string(), + old.to_token_stream().to_string() + ); return DiffResult::CodeChanged; } } + tracing::trace!("found hot reload-able changes {:#?}", rsx_calls); DiffResult::RsxChanged(rsx_calls) } @@ -94,6 +112,9 @@ fn find_rsx_item( (syn::ImplItem::Macro(new_item), syn::ImplItem::Macro(old_item)) => { old_item != new_item } + (syn::ImplItem::Verbatim(stream), syn::ImplItem::Verbatim(stream2)) => { + stream.to_string() != stream2.to_string() + } _ => true, } { return true; @@ -186,10 +207,12 @@ fn find_rsx_trait( } } (syn::TraitItem::Fn(new_item), syn::TraitItem::Fn(old_item)) => { - if let (Some(new_block), Some(old_block)) = (&new_item.default, &old_item.default) { - find_rsx_block(new_block, old_block, rsx_calls) - } else { - true + match (&new_item.default, &old_item.default) { + (Some(new_block), Some(old_block)) => { + find_rsx_block(new_block, old_block, rsx_calls) + } + (None, None) => false, + _ => true, } } (syn::TraitItem::Type(new_item), syn::TraitItem::Type(old_item)) => { @@ -198,6 +221,9 @@ fn find_rsx_trait( (syn::TraitItem::Macro(new_item), syn::TraitItem::Macro(old_item)) => { old_item != new_item } + (syn::TraitItem::Verbatim(stream), syn::TraitItem::Verbatim(stream2)) => { + stream.to_string() != stream2.to_string() + } _ => true, } { return true; @@ -355,6 +381,11 @@ fn find_rsx_expr( || new_expr.or2_token != old_expr.or2_token || new_expr.output != old_expr.output } + (syn::Expr::Const(new_expr), syn::Expr::Const(old_expr)) => { + find_rsx_block(&new_expr.block, &old_expr.block, rsx_calls) + || new_expr.attrs != old_expr.attrs + || new_expr.const_token != old_expr.const_token + } (syn::Expr::Continue(new_expr), syn::Expr::Continue(old_expr)) => old_expr != new_expr, (syn::Expr::Field(new_expr), syn::Expr::Field(old_expr)) => { find_rsx_expr(&new_expr.base, &old_expr.base, rsx_calls) @@ -402,6 +433,7 @@ fn find_rsx_expr( || new_expr.attrs != old_expr.attrs || new_expr.bracket_token != old_expr.bracket_token } + (syn::Expr::Infer(new_expr), syn::Expr::Infer(old_expr)) => new_expr != old_expr, (syn::Expr::Let(new_expr), syn::Expr::Let(old_expr)) => { find_rsx_expr(&new_expr.expr, &old_expr.expr, rsx_calls) || new_expr.attrs != old_expr.attrs @@ -589,7 +621,9 @@ fn find_rsx_expr( _ => true, } } - (syn::Expr::Verbatim(_), syn::Expr::Verbatim(_)) => false, + (syn::Expr::Verbatim(stream), syn::Expr::Verbatim(stream2)) => { + stream.to_string() != stream2.to_string() + } _ => true, } } diff --git a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs index 0393142b4..7e36dddf9 100644 --- a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs +++ b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs @@ -51,17 +51,24 @@ impl FileMap { fn find_rs_files( root: PathBuf, filter: &mut impl FnMut(&Path) -> bool, - ) -> io::Result { + ) -> FileMapSearchResult { let mut files = HashMap::new(); let mut errors = Vec::new(); if root.is_dir() { - for entry in (fs::read_dir(root)?).flatten() { + let read_dir = match fs::read_dir(root) { + Ok(read_dir) => read_dir, + Err(err) => { + errors.push(err); + return FileMapSearchResult { map: files, errors }; + } + }; + for entry in read_dir.flatten() { let path = entry.path(); if !filter(&path) { let FileMapSearchResult { map, errors: child_errors, - } = find_rs_files(path, filter)?; + } = find_rs_files(path, filter); errors.extend(child_errors); files.extend(map); } @@ -69,14 +76,20 @@ impl FileMap { } else if root.extension().and_then(|s| s.to_str()) == Some("rs") { if let Ok(mut file) = File::open(root.clone()) { let mut src = String::new(); - file.read_to_string(&mut src)?; - files.insert(root, (src, None)); + match file.read_to_string(&mut src) { + Ok(_) => { + files.insert(root, (src, None)); + } + Err(err) => { + errors.push(err); + } + } } } - Ok(FileMapSearchResult { map: files, errors }) + FileMapSearchResult { map: files, errors } } - let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter)?; + let FileMapSearchResult { map, errors } = find_rs_files(path, &mut filter); let result = Self { map, in_workspace: HashMap::new(), From 8cf8f66c272e0bc13942d7fa01c1adfae3d2155d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 20 Nov 2023 09:47:03 -0600 Subject: [PATCH 35/62] add more logging to desktop hot reloading --- packages/cli/src/server/desktop/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 978b97d43..be722323b 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -73,6 +73,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option move || { let mut current_child = currently_running_child.write().unwrap(); + log::trace!("Killing old process"); current_child.kill()?; let (child, result) = start_desktop(&config)?; *current_child = child; @@ -212,6 +213,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { // Run the desktop application + log::trace!("Building application"); let result = crate::builder::build_desktop(config, true)?; match &config.executable { @@ -222,6 +224,7 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { if cfg!(windows) { file.set_extension("exe"); } + log::trace!("Running application from {:?}", file); let child = Command::new(file.to_str().unwrap()).spawn()?; Ok((child, result)) From f8ce72c6054701f03786bf55df1bba1a82471884 Mon Sep 17 00:00:00 2001 From: Exotik850 <60969639+Exotik850@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:49:18 -0600 Subject: [PATCH 36/62] Fix outdated lazynode documentation (#1648) * Fix outdated lazynode documentation * Use expanded rsx! instead of just rsx macro --- packages/core/src/lazynodes.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 811dbb734..3189086b4 100644 --- a/packages/core/src/lazynodes.rs +++ b/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 + pub struct LazyNodes<'a, 'b> { #[cfg(not(miri))] inner: SmallBox 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); /// ``` From 7a459e15d8316abfdb5b39d25deb4fff9318fbfe Mon Sep 17 00:00:00 2001 From: ealmloff Date: Tue, 21 Nov 2023 16:28:06 -0600 Subject: [PATCH 37/62] Update packages/hooks/src/use_effect.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marc EspΓ­n --- packages/hooks/src/use_effect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs index 1f5faecdd..6fb2fcf9e 100644 --- a/packages/hooks/src/use_effect.rs +++ b/packages/hooks/src/use_effect.rs @@ -44,7 +44,7 @@ use crate::UseFutureDep; /// async move { /// let user = fetch_user(id).await; /// name.set(user.name); -/// move || println!("Cleaning up from {}", id); +/// move || println!("Cleaning up from {}", id) /// } /// }); /// From 017d7e96bf1ed70463f01d3fb9dc52824179269e Mon Sep 17 00:00:00 2001 From: ealmloff Date: Tue, 21 Nov 2023 16:28:31 -0600 Subject: [PATCH 38/62] restore readme.rs --- examples/readme.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/examples/readme.rs b/examples/readme.rs index b39c04765..f4668488b 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -11,12 +11,6 @@ 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!" } From d9220d4e42b86114096a639ba0b308062b6d763a Mon Sep 17 00:00:00 2001 From: tigerros Date: Thu, 23 Nov 2023 23:44:39 +0100 Subject: [PATCH 39/62] Add it (#1660) --- packages/html/src/global_attributes.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 3792397ad..02b0b32ed 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -269,6 +269,9 @@ trait_methods! { /// azimuth: "azimuth", "style"; + /// + backdrop_filter: "backdrop-filter", "style"; + /// backface_visibility: "backface-visibility", "style"; From 1c0b33cef44678beab17bbcccaeabff48f3c2027 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Nov 2023 09:21:27 -0600 Subject: [PATCH 40/62] fix hot reloading svg elements --- packages/hot-reload/src/file_watcher.rs | 2 +- packages/html/src/elements.rs | 53 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/hot-reload/src/file_watcher.rs b/packages/hot-reload/src/file_watcher.rs index 2509dd789..5b8b76b37 100644 --- a/packages/hot-reload/src/file_watcher.rs +++ b/packages/hot-reload/src/file_watcher.rs @@ -122,7 +122,7 @@ pub fn init(cfg: Config) { } = cfg; if let Ok(crate_dir) = PathBuf::from_str(root_path) { - // try to find the gitingore file + // try to find the gitignore file let gitignore_file_path = crate_dir.join(".gitignore"); let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path); diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 6a0e546fa..5e0d0995d 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -74,7 +74,7 @@ macro_rules! impl_attribute_match { $attr:ident $fil:ident: $vil:ident (in $ns:literal), ) => { if $attr == stringify!($fil) { - return Some((stringify!(fil), Some(ns))); + return Some((stringify!(fil), Some($ns))); } }; } @@ -180,14 +180,26 @@ macro_rules! impl_element_match { }; ( - $el:ident $name:ident $namespace:tt { + $el:ident $name:ident $namespace:literal { $( $fil:ident: $vil:ident $extra:tt, )* } ) => { if $el == stringify!($name) { - return Some((stringify!($name), Some(stringify!($namespace)))); + return dbg!(Some((stringify!($name), Some($namespace)))); + } + }; + + ( + $el:ident $name:ident [$_:literal, $namespace:tt] { + $( + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + if $el == stringify!($name) { + return dbg!(Some((stringify!($name), Some($namespace)))); } }; } @@ -207,6 +219,8 @@ macro_rules! impl_element_match_attributes { $attr $fil: $vil ($extra), ); )* + + return impl_map_global_attributes!($el $attr $name None); } }; @@ -223,10 +237,41 @@ macro_rules! impl_element_match_attributes { $attr $fil: $vil ($extra), ); )* + + return impl_map_global_attributes!($el $attr $name $namespace); } } } +#[cfg(feature = "hot-reload-context")] +macro_rules! impl_map_global_attributes { + ( + $el:ident $attr:ident $element:ident None + ) => { + map_global_attributes($attr) + }; + + ( + $el:ident $attr:ident $element:ident $namespace:literal + ) => { + if $namespace == "http://www.w3.org/2000/svg" { + map_svg_attributes($attr) + } else { + map_global_attributes($attr) + } + }; + + ( + $el:ident $attr:ident $element:ident [$name:literal, $namespace:tt] + ) => { + if $namespace == "http://www.w3.org/2000/svg" { + map_svg_attributes($attr) + } else { + map_global_attributes($attr) + } + }; +} + macro_rules! builder_constructors { ( $( @@ -254,7 +299,7 @@ macro_rules! builder_constructors { } ); )* - map_global_attributes(attribute).or_else(|| map_svg_attributes(attribute)) + None } fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> { From 141554a7861f8c589f246a611f10c332f4e8a711 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Nov 2023 09:23:27 -0600 Subject: [PATCH 41/62] remove logging --- packages/html/src/elements.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 5e0d0995d..d7db36a04 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -187,7 +187,7 @@ macro_rules! impl_element_match { } ) => { if $el == stringify!($name) { - return dbg!(Some((stringify!($name), Some($namespace)))); + return Some((stringify!($name), Some($namespace))); } }; @@ -199,7 +199,7 @@ macro_rules! impl_element_match { } ) => { if $el == stringify!($name) { - return dbg!(Some((stringify!($name), Some($namespace)))); + return Some((stringify!($name), Some($namespace))); } }; } From 18fa1e4831b372ec1cba805de8a1df8626801cd6 Mon Sep 17 00:00:00 2001 From: Exotik850 <60969639+Exotik850@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:42:48 -0600 Subject: [PATCH 42/62] Make UseFuture Clone, factor out dependencies field (#1666) --- packages/hooks/src/use_future.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index 67bcdc74e..b9aa20c31 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -31,13 +31,14 @@ where let state = cx.use_hook(move || UseFuture { update: cx.schedule_update(), - needs_regen: Cell::new(true), + needs_regen: Rc::new(Cell::new(true)), state: val.clone(), task: Default::default(), - dependencies: Vec::new(), }); - if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { + let state_dependencies = cx.use_hook(Vec::new); + + if dependencies.clone().apply(state_dependencies) || state.needs_regen.get() { // kill the old one, if it exists if let Some(task) = state.task.take() { cx.remove_future(task); @@ -69,11 +70,11 @@ pub enum FutureState<'a, T> { Regenerating(&'a T), // the old value } +#[derive(Clone)] pub struct UseFuture { update: Arc, - needs_regen: Cell, + needs_regen: Rc>, task: Rc>>, - dependencies: Vec>, state: UseState>, } From 8e4debb22632f1673c7d943f4382b1ca7b6b9fa9 Mon Sep 17 00:00:00 2001 From: Exotik850 <60969639+Exotik850@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:45:29 -0600 Subject: [PATCH 43/62] remove #[doc(hidden)] from EventHandler (#1665) * remove #[doc(hidden)] from EventHandler * Make UseFuture Clone, factor out dependencies field * Revert "Make UseFuture Clone, factor out dependencies field" This reverts commit 9ca09e595d1cda3f2047750a923a5bdc49a8fa76. * fix formatting --------- Co-authored-by: Evan Almloff --- packages/core/src/events.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3b8edb05c..784a8c865 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -107,8 +107,6 @@ impl std::fmt::Debug for Event { } } -#[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. From 694989e82692e17c1712ddb3e79d6198a73fb3c1 Mon Sep 17 00:00:00 2001 From: Exotik850 <60969639+Exotik850@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:51:09 -0600 Subject: [PATCH 44/62] Add use_const hook (#1667) * Add use_const hook * cargo fmt * clippy allow * pub fn on get_rc --- packages/hooks/src/lib.rs | 3 ++ packages/hooks/src/use_const.rs | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 packages/hooks/src/use_const.rs diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index a5b92a036..8178d4d3d 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -60,6 +60,9 @@ pub mod computed; mod use_on_destroy; pub use use_on_destroy::*; +mod use_const; +pub use use_const::*; + mod use_context; pub use use_context::*; diff --git a/packages/hooks/src/use_const.rs b/packages/hooks/src/use_const.rs new file mode 100644 index 000000000..47ddb1ed2 --- /dev/null +++ b/packages/hooks/src/use_const.rs @@ -0,0 +1,76 @@ +use std::rc::Rc; + +use dioxus_core::prelude::*; + +/// Store constant state between component renders. +/// +/// UseConst allows you to store state that is initialized once and then remains constant across renders. +/// You can only get an immutable reference after initalization. +/// This can be useful for values that don't need to update reactively, thus can be memoized easily +/// +/// ```rust, ignore +/// struct ComplexData(i32); +/// +/// fn Component(cx: Scope) -> Element { +/// let id = use_const(cx, || ComplexData(100)); +/// +/// cx.render(rsx! { +/// div { "{id.0}" } +/// }) +/// } +/// ``` +#[must_use] +pub fn use_const( + cx: &ScopeState, + initial_state_fn: impl FnOnce() -> T, +) -> &UseConst { + cx.use_hook(|| UseConst { + value: Rc::new(initial_state_fn()), + }) +} + +#[derive(Clone)] +pub struct UseConst { + value: Rc, +} + +impl PartialEq for UseConst { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.value, &other.value) + } +} + +impl core::fmt::Display for UseConst { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +impl UseConst { + pub fn get_rc(&self) -> &Rc { + &self.value + } +} + +impl std::ops::Deref for UseConst { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.value.as_ref() + } +} + +#[test] +fn use_const_makes_sense() { + #[allow(unused)] + + fn app(cx: Scope) -> Element { + let const_val = use_const(cx, || vec![0, 1, 2, 3]); + + assert!(const_val[0] == 0); + + // const_val.remove(0); // Cannot Compile, cannot get mutable reference now + + None + } +} From b4fe3829d18a8a8d6ce1a293da12e93bd94c76f5 Mon Sep 17 00:00:00 2001 From: Exotik850 <60969639+Exotik850@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:41:14 -0600 Subject: [PATCH 45/62] Add cargo make tidy/miri (#1670) * Add cargo make tidy/miri * fmt * seperate tidy and tests --- Makefile.toml | 74 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 6f331b98f..1bfb02217 100644 --- a/Makefile.toml +++ b/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 From e0fbed7eea90592d2769e8e763b69aea58af0106 Mon Sep 17 00:00:00 2001 From: Exotik850 <60969639+Exotik850@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:54:17 -0600 Subject: [PATCH 46/62] Switch outdated tui crate for ratatui (#1671) * Switch outdated tui crate for ratatui * fix query docs whoops --- packages/rink/Cargo.toml | 2 +- packages/rink/src/lib.rs | 6 +++--- packages/rink/src/render.rs | 7 +++---- packages/rink/src/style.rs | 3 ++- packages/rink/src/style_attributes.rs | 8 ++++---- packages/rink/src/widget.rs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/rink/Cargo.toml b/packages/rink/Cargo.toml index ba3d549f8..92d1eae8c 100644 --- a/packages/rink/Cargo.toml +++ b/packages/rink/Cargo.toml @@ -14,7 +14,7 @@ dioxus-html = { workspace = true } dioxus-native-core = { workspace = true, features = ["layout-attributes"] } dioxus-native-core-macro = { workspace = true } -tui = "0.17.0" +ratatui = "0.24.0" crossterm = "0.26.1" anyhow = "1.0.42" tokio = { workspace = true, features = ["full"] } diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 2eba1b98a..4a7261b55 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -17,6 +17,7 @@ use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt}; use futures_channel::mpsc::unbounded; use layout::TaffyLayout; use prevent_default::PreventDefault; +use ratatui::{backend::CrosstermBackend, Terminal}; use std::{io, time::Duration}; use std::{ pin::Pin, @@ -26,7 +27,6 @@ use std::{rc::Rc, sync::RwLock}; use style_attributes::StyleModifier; pub use taffy::{geometry::Point, prelude::*}; use tokio::select; -use tui::{backend::CrosstermBackend, Terminal}; use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject}; mod config; @@ -180,7 +180,7 @@ pub fn render( if !to_rerender.is_empty() || updated { updated = false; - fn resize(dims: tui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) { + fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) { let width = screen_to_layout_space(dims.width); let height = screen_to_layout_space(dims.height); let root_node = rdom @@ -222,7 +222,7 @@ pub fn render( } else { let rdom = rdom.read().unwrap(); resize( - tui::layout::Rect { + ratatui::layout::Rect { x: 0, y: 0, width: 1000, diff --git a/packages/rink/src/render.rs b/packages/rink/src/render.rs index 19d642720..4185f3a62 100644 --- a/packages/rink/src/render.rs +++ b/packages/rink/src/render.rs @@ -1,11 +1,10 @@ use dioxus_native_core::{prelude::*, tree::TreeRef}; -use std::io::Stdout; +use ratatui::{layout::Rect, style::Color}; use taffy::{ geometry::Point, prelude::{Dimension, Layout, Size}, Taffy, }; -use tui::{backend::CrosstermBackend, layout::Rect, style::Color}; use crate::{ focus::Focused, @@ -20,7 +19,7 @@ use crate::{ const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; pub(crate) fn render_vnode( - frame: &mut tui::Frame>, + frame: &mut ratatui::Frame, layout: &Taffy, node: NodeRef, cfg: Config, @@ -96,7 +95,7 @@ pub(crate) fn render_vnode( impl RinkWidget for NodeRef<'_> { fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { - use tui::symbols::line::*; + use ratatui::symbols::line::*; enum Direction { Left, diff --git a/packages/rink/src/style.rs b/packages/rink/src/style.rs index 857322079..905cc4b34 100644 --- a/packages/rink/src/style.rs +++ b/packages/rink/src/style.rs @@ -1,6 +1,6 @@ use std::{num::ParseFloatError, str::FromStr}; -use tui::style::{Color, Modifier, Style}; +use ratatui::style::{Color, Modifier, Style}; use crate::RenderingMode; @@ -442,6 +442,7 @@ impl RinkStyle { impl From for Style { fn from(val: RinkStyle) -> Self { Style { + underline_color: None, fg: val.fg.map(|c| c.color), bg: val.bg.map(|c| c.color), add_modifier: val.add_modifier, diff --git a/packages/rink/src/style_attributes.rs b/packages/rink/src/style_attributes.rs index 0c87e3f83..d1e8c6bee 100644 --- a/packages/rink/src/style_attributes.rs +++ b/packages/rink/src/style_attributes.rs @@ -187,8 +187,8 @@ pub enum BorderStyle { } impl BorderStyle { - pub fn symbol_set(&self) -> Option { - use tui::symbols::line::*; + pub fn symbol_set(&self) -> Option { + use ratatui::symbols::line::*; const DASHED: Set = Set { horizontal: "β•Œ", vertical: "β•Ž", @@ -570,7 +570,7 @@ fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifier) { } fn apply_font(name: &str, value: &str, style: &mut StyleModifier) { - use tui::style::Modifier; + use ratatui::style::Modifier; match name { "font" => (), "font-family" => (), @@ -593,7 +593,7 @@ fn apply_font(name: &str, value: &str, style: &mut StyleModifier) { } fn apply_text(name: &str, value: &str, style: &mut StyleModifier) { - use tui::style::Modifier; + use ratatui::style::Modifier; match name { "text-align" => todo!(), diff --git a/packages/rink/src/widget.rs b/packages/rink/src/widget.rs index ab9044998..91af5b4f6 100644 --- a/packages/rink/src/widget.rs +++ b/packages/rink/src/widget.rs @@ -1,4 +1,4 @@ -use tui::{ +use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Modifier}, From be94c69f116ede6bec01373b1a47277256fd2db9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 29 Nov 2023 11:38:28 -0600 Subject: [PATCH 47/62] make optional props accept T or Option --- examples/optional_props.rs | 6 +++++ packages/core-macro/src/props/mod.rs | 33 ++++++++-------------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/examples/optional_props.rs b/examples/optional_props.rs index c5652a2e7..d5cf1e711 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -12,6 +12,12 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx! { + Button { + a: "asd".to_string(), + c: "asd".to_string(), + d: Some("asd".to_string()), + e: Some("asd".to_string()), + } Button { a: "asd".to_string(), c: "asd".to_string(), diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 6e6c43d66..f8450e36a 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -783,31 +783,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!( From 533c7bab492d121b79b126458429fc5774c3a39e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 29 Nov 2023 11:39:38 -0600 Subject: [PATCH 48/62] show more cases in the optional props example --- examples/optional_props.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/optional_props.rs b/examples/optional_props.rs index d5cf1e711..8108d437d 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -20,10 +20,16 @@ fn app(cx: Scope) -> Element { } 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()), + } }) } From a840e012d81d6b2de5b3ec7fcd91a65552c40a18 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 29 Nov 2023 11:46:18 -0600 Subject: [PATCH 49/62] fix clippy --- packages/core-macro/src/props/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f8450e36a..d7a580705 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/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)] From c8a9a7b1d5cf06c59c90ee54d5ccbe12f06c6b7a Mon Sep 17 00:00:00 2001 From: zhangzhonglai Date: Fri, 1 Dec 2023 21:52:20 +0800 Subject: [PATCH 50/62] docs(desktop): fix wrong example code (#1678) * docs(desktop): fix wrong example code * stop ignoring doctests in dioxus desktop --------- Co-authored-by: ealmloff --- packages/desktop/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index bfefaf547..63a8914f3 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -54,7 +54,7 @@ use wry::{application::window::WindowId, webview::WebContext}; /// /// This function will start a multithreaded Tokio runtime as well the WebView event loop. /// -/// ```rust, ignore +/// ```rust, no_run /// use dioxus::prelude::*; /// /// fn main() { @@ -77,11 +77,12 @@ pub fn launch(root: Component) { /// /// You can configure the WebView window with a configuration closure /// -/// ```rust, ignore +/// ```rust, no_run /// use dioxus::prelude::*; +/// use dioxus_desktop::*; /// /// fn main() { -/// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App"))); +/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App"))); /// } /// /// fn app(cx: Scope) -> Element { @@ -100,8 +101,9 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// /// You can configure the WebView window with a configuration closure /// -/// ```rust, ignore +/// ```rust, no_run /// use dioxus::prelude::*; +/// use dioxus_desktop::Config; /// /// fn main() { /// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); From a2ca1760c9f679c8904df2d4a9bde9c43a8697a4 Mon Sep 17 00:00:00 2001 From: Felix F Xu <84662027+felixf4xu@users.noreply.github.com> Date: Sun, 3 Dec 2023 09:25:27 +0800 Subject: [PATCH 51/62] Set local_socket_stream to blocking (default) (#1682) --- packages/cli/src/server/desktop/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 978b97d43..98d6e9244 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -121,9 +121,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 From d404ddfccfdacc57ec6043079fa1f07f3a50b947 Mon Sep 17 00:00:00 2001 From: Felix F Xu <84662027+felixf4xu@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:02:30 +0800 Subject: [PATCH 52/62] fix false notification/rebuild for dx hot-reload (#1684) * fix false notification/rebuild for dx hot-reload * Make sure we have permissions and the changed file exists before reading the metadata --------- Co-authored-by: ealmloff --- packages/cli/src/server/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 79a5531b6..eb4021184 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -55,6 +55,16 @@ async fn setup_file_watcher Result + 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); From e69a4a7cd84da0ac371d4933f7704493c5a9735a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 07:07:35 -0600 Subject: [PATCH 53/62] Bump JamesIves/github-pages-deploy-action from 4.4.3 to 4.5.0 (#1685) Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.4.3 to 4.5.0. - [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases) - [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.4.3...v4.5.0) --- updated-dependencies: - dependency-name: JamesIves/github-pages-deploy-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs stable.yml | 2 +- .github/workflows/docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml index 642216f0f..523a24067 100644 --- a/.github/workflows/docs stable.yml +++ b/.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. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 88542aea8..7701209c1 100644 --- a/.github/workflows/docs.yml +++ b/.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. From 8149868ebc8610c4c2bc6fc41b50d9be4f4bfb40 Mon Sep 17 00:00:00 2001 From: Raul Cesar Teixeira Date: Mon, 4 Dec 2023 21:11:02 -0300 Subject: [PATCH 54/62] Fixed conflict of "--verbose" and "--quiet" flags (#1686) Co-authored-by: Raul Cesar --- packages/cli/src/builder.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 71be8f115..08e636237 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -54,8 +54,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .arg("build") .arg("--target") .arg("wasm32-unknown-unknown") - .arg("--message-format=json") - .arg("--quiet"); + .arg("--message-format=json"); let cmd = if config.release { cmd.arg("--release") @@ -65,7 +64,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { let cmd = if config.verbose { cmd.arg("--verbose") } else { - cmd + cmd.arg("--quiet") }; let cmd = if config.custom_profile.is_some() { From 04fd2487b3edaf3c8f5cc347562de942fb10504c Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Tue, 5 Dec 2023 20:10:18 +0000 Subject: [PATCH 55/62] feat(fullstack): add `render_handler_with_state` (#1687) * feat(fullstack): add `render_handler_with_state` When using server functions, the current pattern to access state such as database connections is to use `register_server_fns_with_handler` on an Axum router and 'inject' the state into the context provided to the server function. However, this only affects function calls which go via the Axum router; SSR renders bypass this, and therefore don't have access to any state. This commit adds an alternative `render_handler` which accepts some additional state. That state is injected into the context in a similar manner to `register_server_fns_with_handler`. SSR renders can then proceed to run in the same way as HTTP calls. * Change state object to 'inject_state' callback Also add a compiling doctest example. * remove the explicit for<'a> lifetime * remove unused assets_path from render_handler_with_context example --------- Co-authored-by: Evan Almloff --- .../fullstack/src/adapters/axum_adapter.rs | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index 95d9ee6cd..41a7c92be 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -369,15 +369,65 @@ fn apply_request_parts_to_response( } } -/// SSR renderer handler for Axum -pub async fn render_handler( - State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, +/// SSR renderer handler for Axum with added context injection. +/// +/// # Example +/// ```rust,no_run +/// #![allow(non_snake_case)] +/// use std::sync::{Arc, Mutex}; +/// +/// use axum::routing::get; +/// use dioxus::prelude::*; +/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*}; +/// +/// fn app(cx: Scope) -> Element { +/// render! { +/// "hello!" +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() { +/// let cfg = ServeConfigBuilder::new(app, ()) +/// .assets_path("dist") +/// .build(); +/// let ssr_state = SSRState::new(&cfg); +/// +/// // This could be any state you want to be accessible from your server +/// // functions using `[DioxusServerContext::get]`. +/// let state = Arc::new(Mutex::new("state".to_string())); +/// +/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); +/// axum::Server::bind(&addr) +/// .serve( +/// axum::Router::new() +/// // Register server functions, etc. +/// // Note you probably want to use `register_server_fns_with_handler` +/// // to inject the context into server functions running outside +/// // of an SSR render context. +/// .fallback(get(render_handler_with_context).with_state(( +/// move |ctx| ctx.insert(state.clone()).unwrap(), +/// cfg, +/// ssr_state, +/// ))) +/// .into_make_service(), +/// ) +/// .await +/// .unwrap(); +/// } +/// ``` +pub async fn render_handler_with_context< + P: Clone + serde::Serialize + Send + Sync + 'static, + F: FnMut(&mut DioxusServerContext), +>( + State((mut inject_context, cfg, ssr_state)): State<(F, ServeConfig

, SSRState)>, request: Request, ) -> impl IntoResponse { let (parts, _) = request.into_parts(); let url = parts.uri.path_and_query().unwrap().to_string(); let parts: Arc> = Arc::new(RwLock::new(parts.into())); - let server_context = DioxusServerContext::new(parts.clone()); + let mut server_context = DioxusServerContext::new(parts.clone()); + inject_context(&mut server_context); match ssr_state.render(url, &cfg, &server_context).await { Ok(rendered) => { @@ -395,6 +445,14 @@ pub async fn render_handler } } +/// SSR renderer handler for Axum +pub async fn render_handler( + State((cfg, ssr_state)): State<(ServeConfig

, SSRState)>, + request: Request, +) -> impl IntoResponse { + render_handler_with_context(State((|_: &mut _| (), cfg, ssr_state)), request).await +} + fn report_err(e: E) -> Response { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) From 0c9248688372cdefd3fedb8bc884ee3fcb3ddcca Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Dec 2023 22:57:20 -0800 Subject: [PATCH 56/62] publish generational box --- Cargo.toml | 4 ++-- packages/generational-box/Cargo.toml | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d687d74d0..153866883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,8 +76,8 @@ plasmo = { path = "packages/rink", version = "0.4.0" } 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" } +dioxus-signals = { path = "packages/signals", version = "0.1.0" } +generational-box = { path = "packages/generational-box", version = "0.1.0" } 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" } diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml index 6c5a5a841..661c9725b 100644 --- a/packages/generational-box/Cargo.toml +++ b/packages/generational-box/Cargo.toml @@ -1,9 +1,12 @@ [package] name = "generational-box" authors = ["Evan Almloff"] -version = "0.0.0" +version = "0.1.0" edition = "2018" - +description = "A box backed by a generational runtime" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +keywords = ["generational", "box", "memory", "allocator"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] From 6088483fb2cdd27a0c536a6a596f0c986fe23927 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Dec 2023 23:01:10 -0800 Subject: [PATCH 57/62] fix signals import --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 153866883..06e4ca1fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ plasmo = { path = "packages/rink", version = "0.4.0" } 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", version = "0.1.0" } +dioxus-signals = { path = "packages/signals" } generational-box = { path = "packages/generational-box", version = "0.1.0" } dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" } dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" } From 8530f53692ec4ce2df6dcec4fb2af85576d6fdac Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Wed, 6 Dec 2023 04:55:07 -0800 Subject: [PATCH 58/62] Fix typo (#1695) * Fix typo * Fix typo again --- packages/core/src/mutations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 27976c157..f33590e2e 100644 --- a/packages/core/src/mutations.rs +++ b/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 { From ab1fab5f50e5e9b487ce2f7e4535d659b1d3e274 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Dec 2023 16:49:05 -0800 Subject: [PATCH 59/62] No publish demos --- examples/openid_connect_demo/Cargo.toml | 1 + examples/query_segments_demo/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/openid_connect_demo/Cargo.toml b/examples/openid_connect_demo/Cargo.toml index 4c5f47061..2a1abe173 100644 --- a/examples/openid_connect_demo/Cargo.toml +++ b/examples/openid_connect_demo/Cargo.toml @@ -2,6 +2,7 @@ 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 diff --git a/examples/query_segments_demo/Cargo.toml b/examples/query_segments_demo/Cargo.toml index 4a7d784c7..ee9f49c70 100644 --- a/examples/query_segments_demo/Cargo.toml +++ b/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 From d3f6ff703c0f90cd58da8d459be056db29f3c38e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Dec 2023 16:56:35 -0800 Subject: [PATCH 60/62] Release 0.4.3 dioxus@0.4.3 dioxus-autofmt@0.4.3 dioxus-check@0.4.3 dioxus-cli@0.4.3 dioxus-core@0.4.3 dioxus-core-macro@0.4.3 dioxus-desktop@0.4.3 dioxus-fullstack@0.4.3 dioxus-hooks@0.4.3 dioxus-hot-reload@0.4.3 dioxus-html@0.4.3 dioxus-interpreter-js@0.4.3 dioxus-liveview@0.4.3 dioxus-mobile@0.4.3 dioxus-native-core@0.4.3 dioxus-native-core-macro@0.4.3 dioxus-router@0.4.3 dioxus-router-macro@0.4.3 dioxus-rsx@0.4.3 dioxus-signals@0.4.3 dioxus-ssr@0.4.3 dioxus-tui@0.4.3 dioxus-web@0.4.3 dioxus_server_macro@0.4.3 fermi@0.4.3 generational-box@0.4.3 plasmo@0.4.3 rsx-rosetta@0.4.3 Generated by cargo-workspaces --- Cargo.toml | 6 +++--- examples/tailwind/Cargo.toml | 2 +- packages/cli/Cargo.toml | 2 +- packages/generational-box/Cargo.toml | 2 +- packages/signals/Cargo.toml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 06e4ca1fa..1772c1f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,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] @@ -77,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", version = "0.1.0" } +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" } @@ -99,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" diff --git a/examples/tailwind/Cargo.toml b/examples/tailwind/Cargo.toml index 5c2fbe666..26ac202f1 100644 --- a/examples/tailwind/Cargo.toml +++ b/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" } \ No newline at end of file +dioxus-web = { path = "../../packages/web" } diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index adb0239f2..128133602 100644 --- a/packages/cli/Cargo.toml +++ b/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" diff --git a/packages/generational-box/Cargo.toml b/packages/generational-box/Cargo.toml index 661c9725b..f070c1805 100644 --- a/packages/generational-box/Cargo.toml +++ b/packages/generational-box/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "generational-box" authors = ["Evan Almloff"] -version = "0.1.0" +version = "0.4.3" edition = "2018" description = "A box backed by a generational runtime" license = "MIT OR Apache-2.0" diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 56e19bbd8..34ae671b3 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dioxus-signals" authors = ["Jonathan Kelley"] -version = "0.0.0" +version = "0.4.3" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,4 +20,4 @@ tokio = { version = "1", features = ["full"] } [features] default = [] -serialize = ["serde"] \ No newline at end of file +serialize = ["serde"] From 16982c9f69adcebae9d9f086328bb20b11d6882b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Dec 2023 20:54:50 -0800 Subject: [PATCH 61/62] Add metadata for signals crate --- packages/signals/Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/signals/Cargo.toml b/packages/signals/Cargo.toml index 34ae671b3..c85fb6d7f 100644 --- a/packages/signals/Cargo.toml +++ b/packages/signals/Cargo.toml @@ -3,6 +3,12 @@ name = "dioxus-signals" authors = ["Jonathan Kelley"] version = "0.4.3" edition = "2018" +description = "Signals for Dioxus" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +keywords = ["dom", "ui", "gui", "react", "wasm"] +rust-version = "1.60.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 69bf6b9dd7a8872e38a846ea8094d76589d05b21 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 6 Dec 2023 21:27:26 -0800 Subject: [PATCH 62/62] CI: adjust matrix test to install rustfmt during autofmt tests --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 037525557..1b1f1ed7b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,8 +124,6 @@ jobs: } steps: - - uses: actions/checkout@v4 - - name: install stable uses: dtolnay/rust-toolchain@master with: @@ -141,6 +139,11 @@ jobs: workspaces: core -> ../target save-if: ${{ matrix.features.key == 'all' }} + - name: Install rustfmt + run: rustup component add rustfmt + + - uses: actions/checkout@v4 + - name: test run: | ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}