2
0
Fork 0
mirror of https://github.com/DioxusLabs/dioxus synced 2025-02-18 14:48:26 +00:00

Merge branch 'main' into isomorphic-spawn

This commit is contained in:
Evan Almloff 2024-03-11 15:42:47 -05:00
commit 3e9a360b55
22 changed files with 826 additions and 931 deletions
README.md
examples
packages
cli/src/server
config-macro/src
core-macro
.vscode
src
extension
fullstack/src
hooks/src
signals
translations/tr-tr

View file

@ -47,6 +47,8 @@
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/pt-br/README.md"> PT-BR </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/ja-jp/README.md"> 日本語 </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/tr-tr/README.md"> Türkçe </a>
</h3>
</div>
@ -145,7 +147,7 @@ There's tons of options for building apps, so why would you choose Dioxus?
Well, first and foremost, Dioxus prioritizes developer experience. This is reflected in a variety of features unique to Dioxus:
- Autoformatting of our meta language (RSX) and accompanying VSCode extension
- Autoformatting of our meta language (RSX) and accompanying [VSCode extension](https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus)
- Hotreloading using an interpreter of RSX for both desktop and web
- Emphasis on good docs - our guide is complete and our HTML elements are documented
- Significant research in simplifying

View file

@ -1,7 +1,7 @@
//! The example from the readme!
//!
//! This example demonstrates how to create a simple counter app with dioxus. The `Signal` type wraps inner values,
//! making them `Copy`, allowing them to be freely used in closures and and async functions. `Signal` also provides
//! making them `Copy`, allowing them to be freely used in closures and async functions. `Signal` also provides
//! helper methods like AddAssign, SubAssign, toggle, etc, to make it easy to update the value without running
//! into lock issues.

View file

@ -47,8 +47,13 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
for path in &e.paths {
// if this is not a rust file, rebuild the whole project
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
let path_extension = path.extension().and_then(|p| p.to_str());
if path_extension != Some("rs") {
needs_full_rebuild = true;
// if backup file generated will impact normal hot-reload, so ignore it
if path_extension == Some("rs~") {
needs_full_rebuild = false;
}
break;
}

View file

@ -15,7 +15,7 @@ pub fn server_only(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -30,7 +30,7 @@ pub fn client(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -45,7 +45,7 @@ pub fn web(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -60,7 +60,7 @@ pub fn desktop(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -75,7 +75,7 @@ pub fn fullstack(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -90,7 +90,7 @@ pub fn ssr(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -105,7 +105,7 @@ pub fn liveview(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()
@ -120,7 +120,7 @@ pub fn tui(input: TokenStream) -> TokenStream {
}
} else {
quote! {
()
|| {}
}
}
.into()

View file

@ -0,0 +1,20 @@
{
"rust-analyzer.check.workspace": false,
// "rust-analyzer.check.command": "check alsjdlaskdjljkasd",
"rust-analyzer.check.allTargets": false,
"rust-analyzer.cargo.buildScripts.enable": true,
"rust-analyzer.cargo.buildScripts.overrideCommand": [
"cargo",
"check",
"--quiet",
// "--package",
// "dioxus-core-macro",
"--message-format",
"json",
"--all-targets"
],
"rust-analyzer.cargo.buildScripts.rebuildOnSave": false,
"rust-analyzer.cargo.buildScripts.invocationLocation": "root",
"rust-analyzer.cargo.buildScripts.invocationStrategy": "once",
// "rust-analyzer.check.command": "check --package dioxus-core-macro"
}

View file

@ -0,0 +1,387 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::*;
pub struct ComponentBody {
pub item_fn: ItemFn,
}
impl Parse for ComponentBody {
fn parse(input: ParseStream) -> Result<Self> {
let item_fn: ItemFn = input.parse()?;
validate_component_fn_signature(&item_fn)?;
Ok(Self { item_fn })
}
}
impl ToTokens for ComponentBody {
fn to_tokens(&self, tokens: &mut TokenStream) {
let comp_fn = self.comp_fn();
// If there's no props declared, we simply omit the props argument
// This is basically so you can annotate the App component with #[component] and still be compatible with the
// launch signatures that take fn() -> Element
let props_struct = match self.item_fn.sig.inputs.is_empty() {
// No props declared, so we don't need to generate a props struct
true => quote! {},
// Props declared, so we generate a props struct and thatn also attach the doc attributes to it
false => {
let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
let props_struct = self.props_struct();
quote! {
#[doc = #doc]
#props_struct
}
}
};
tokens.append_all(quote! {
#props_struct
#[allow(non_snake_case)]
#comp_fn
});
}
}
impl ComponentBody {
// build a new item fn, transforming the original item fn
fn comp_fn(&self) -> ItemFn {
let ComponentBody { item_fn, .. } = self;
let ItemFn {
attrs,
vis,
sig,
block,
} = item_fn;
let Signature {
inputs,
ident: fn_ident,
generics,
output: fn_output,
asyncness,
..
} = sig;
let Generics { where_clause, .. } = generics;
let (_, ty_generics, _) = generics.split_for_impl();
// We generate a struct with the same name as the component but called `Props`
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
// We pull in the field names from the original function signature, but need to strip off the mutability
let struct_field_names = inputs.iter().filter_map(rebind_mutability);
let props_docs = self.props_docs(inputs.iter().skip(1).collect());
// Don't generate the props argument if there are no inputs
// This means we need to skip adding the argument to the function signature, and also skip the expanded struct
let props_ident = match inputs.is_empty() {
true => quote! {},
false => quote! { mut __props: #struct_ident #ty_generics },
};
let expanded_struct = match inputs.is_empty() {
true => quote! {},
false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; },
};
parse_quote! {
#(#attrs)*
#(#props_docs)*
#asyncness #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause {
#expanded_struct
#block
}
}
}
/// Build an associated struct for the props of the component
///
/// This will expand to the typed-builder implementation that we have vendored in this crate.
/// TODO: don't vendor typed-builder and instead transform the tokens we give it before expansion.
/// TODO: cache these tokens since this codegen is rather expensive (lots of tokens)
///
/// We try our best to transfer over any declared doc attributes from the original function signature onto the
/// props struct fields.
fn props_struct(&self) -> ItemStruct {
let ItemFn { vis, sig, .. } = &self.item_fn;
let Signature {
inputs,
ident,
generics,
..
} = sig;
let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
parse_quote! {
#[derive(Props, Clone, PartialEq)]
#[allow(non_camel_case_types)]
#vis struct #struct_ident #generics {
#(#struct_fields),*
}
}
}
/// Convert a list of function arguments into a list of doc attributes for the props struct
///
/// This lets us generate set of attributes that we can apply to the props struct to give it a nice docstring.
fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
let fn_ident = &self.item_fn.sig.ident;
if inputs.len() <= 1 {
return Vec::new();
}
let arg_docs = inputs
.iter()
.filter_map(|f| build_doc_fields(f))
.collect::<Vec<_>>();
let mut props_docs = Vec::with_capacity(5);
let props_def_link = fn_ident.to_string() + "Props";
let header =
format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
props_docs.push(parse_quote! {
#[doc = #header]
});
for arg in arg_docs {
let DocField {
arg_name,
arg_type,
deprecation,
input_arg_doc,
} = arg;
let arg_name = arg_name.into_token_stream().to_string();
let arg_type = crate::utils::format_type_string(arg_type);
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
.replace("\n\n", "</p><p>");
let prop_def_link = format!("{props_def_link}::{arg_name}");
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
if let Some(deprecation) = deprecation {
arg_doc.push_str("<p>👎 Deprecated");
if let Some(since) = deprecation.since {
arg_doc.push_str(&format!(" since {since}"));
}
if let Some(note) = deprecation.note {
let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
arg_doc.push_str(&format!(": {note}"));
}
arg_doc.push_str("</p>");
if !input_arg_doc.is_empty() {
arg_doc.push_str("<hr/>");
}
}
if !input_arg_doc.is_empty() {
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
}
props_docs.push(parse_quote! { #[doc = #arg_doc] });
}
props_docs
}
}
struct DocField<'a> {
arg_name: &'a Pat,
arg_type: &'a Type,
deprecation: Option<crate::utils::DeprecatedAttribute>,
input_arg_doc: String,
}
fn build_doc_fields(f: &FnArg) -> Option<DocField> {
let FnArg::Typed(pt) = f else { unreachable!() };
let arg_doc = pt
.attrs
.iter()
.filter_map(|attr| {
// TODO: Error reporting
// Check if the path of the attribute is "doc"
if !is_attr_doc(attr) {
return None;
};
let Meta::NameValue(meta_name_value) = &attr.meta else {
return None;
};
let Expr::Lit(doc_lit) = &meta_name_value.value else {
return None;
};
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
return None;
};
Some(doc_lit_str.value())
})
.fold(String::new(), |mut doc, next_doc_line| {
doc.push('\n');
doc.push_str(&next_doc_line);
doc
});
Some(DocField {
arg_name: &pt.pat,
arg_type: &pt.ty,
deprecation: pt.attrs.iter().find_map(|attr| {
if attr.path() != &parse_quote!(deprecated) {
return None;
}
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
match res {
Err(e) => panic!("{}", e.to_string()),
Ok(v) => Some(v),
}
}),
input_arg_doc: arg_doc,
})
}
fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
// Do some validation....
// 1. Ensure the component returns *something*
if item_fn.sig.output == ReturnType::Default {
return Err(Error::new(
item_fn.sig.output.span(),
"Must return a <dioxus_core::Element>".to_string(),
));
}
// 2. make sure there's no lifetimes on the component - we don't know how to handle those
if item_fn.sig.generics.lifetimes().count() > 0 {
return Err(Error::new(
item_fn.sig.generics.span(),
"Lifetimes are not supported in components".to_string(),
));
}
// 3. we can't handle async components
if item_fn.sig.asyncness.is_some() {
return Err(Error::new(
item_fn.sig.asyncness.span(),
"Async components are not supported".to_string(),
));
}
// 4. we can't handle const components
if item_fn.sig.constness.is_some() {
return Err(Error::new(
item_fn.sig.constness.span(),
"Const components are not supported".to_string(),
));
}
// 5. no receiver parameters
if item_fn
.sig
.inputs
.iter()
.any(|f| matches!(f, FnArg::Receiver(_)))
{
return Err(Error::new(
item_fn.sig.inputs.span(),
"Receiver parameters are not supported".to_string(),
));
}
Ok(())
}
/// Convert a function arg with a given visibility (provided by the function) and then generate a field for the
/// associated props struct.
fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
// There's no receivers (&self) allowed in the component body
let FnArg::Typed(pt) = f else { unreachable!() };
let arg_pat = match pt.pat.as_ref() {
// rip off mutability
// todo: we actually don't want any of the extra bits of the field pattern
Pat::Ident(f) => {
let mut f = f.clone();
f.mutability = None;
quote! { #f }
}
a => quote! { #a },
};
let PatType {
attrs,
ty,
colon_token,
..
} = pt;
quote! {
#(#attrs)*
#vis #arg_pat #colon_token #ty
}
}
fn rebind_mutability(f: &FnArg) -> Option<TokenStream> {
// There's no receivers (&self) allowed in the component body
let FnArg::Typed(pt) = f else { unreachable!() };
let pat = &pt.pat;
let mut pat = pat.clone();
// rip off mutability, but still write it out eventually
if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
pat_ident.mutability = None;
}
Some(quote!(mut #pat))
}
/// Checks if the attribute is a `#[doc]` attribute.
fn is_attr_doc(attr: &Attribute) -> bool {
attr.path() == &parse_quote!(doc)
}
fn keep_up_to_n_consecutive_chars(
input: &str,
n_of_consecutive_chars_allowed: usize,
target_char: char,
) -> String {
let mut output = String::new();
let mut prev_char: Option<char> = None;
let mut consecutive_count = 0;
for c in input.chars() {
match prev_char {
Some(prev) if c == target_char && prev == target_char => {
if consecutive_count < n_of_consecutive_chars_allowed {
output.push(c);
consecutive_count += 1;
}
}
_ => {
output.push(c);
prev_char = Some(c);
consecutive_count = 1;
}
}
}
output
}

View file

@ -1,204 +0,0 @@
//! This module is used for parsing a component function into a struct that is subsequently
//! deserialized into something useful using deserializer arguments.
//!
//! Let's break that down with a term glossary and examples which show usage and implementing.
//!
//! # Glossary
//! * `component body` - The [`ComponentBody`] struct. It's used to parse a component function [`proc_macro::TokenStream`]
//! to a reusable struct that deserializers use to modify the token stream.
//! * `deserializer` - A struct that deserializes the [`ComponentBody`] into a [`DeserializerOutput`].
//! It implements the [`DeserializerArgs`] trait, but as you can see, it's called "DeserializerArgs",
//! not "Deserializer". Why?
//! Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
//! takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
//! * `deserializer output` - A struct that implements the [`DeserializerOutput`] trait.
//! This struct is what enables deserializers to use each other, since it contains the fields that
//! a deserializer needs to turn a token stream to a different token stream.
//! This means a deserializer can get the output of another deserializer, and use that output,
//! thereby using the functionality of a different deserializer.
//! This struct also implements [`ToTokens`], which means that this is the final stage of the whole process.
//!
//! # Examples
//! *Not all imports might be included.*
//!
//! ## Usage in a procedural macro attribute
//! ```rs,ignore
//! use proc_macro::TokenStream;
//!
//! // Some documentation. You can reuse this in your deserializer structs.
//! /// This attribute changes the name of a component function to whatever the first argument is.
//! #[proc_macro_attribute]
//! pub fn name_changer(args: TokenStream, input: TokenStream) -> TokenStream {
//! // Parse the component body.
//! let component_body = parse_macro_input!(input as ComponentBody);
//!
//! // Parse the first argument, which is going to be the components new name.
//! let new_name: String = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
//! Err(e) => return e.to_compile_error().into(), // Convert to a compile error and return
//! Ok(args) => {
//! // If the argument exists, then convert it to a string
//! if let Some(first) = args.first() {
//! first.to_token_stream().to_string()
//! } else {
//! // If the argument doesn't exist, return an error with the appropriate message.
//! // The "span" is the location of some code.
//! // The error occurred in the "args" token stream, so point the error there.
//! return Error::new(args.span(), "No new name provided").to_compile_error().into();
//! }
//! }
//! };
//!
//! let new_name = &*new_name;
//!
//! // Deserialize the component body to an output with the given args.
//! let output = component_body.deserialize(NameChangerDeserializerArgs { new_name });
//!
//! // Error handling like before, except now you're ready to return the final value.
//! match output {
//! Err(e) => e.to_compile_error().into(),
//! Ok(output) => output.to_token_stream().into(),
//! }
//! }
//! ```
//! ## Using the macro in Dioxus code:
//! ```rs
//! use your_proc_macro_library::name_changer;
//! use dioxus::prelude::*;
//!
//! #[name_changer(CoolName)]
//! pub fn LameName() -> Element {
//! rsx! { "I want a cool name!" }
//! }
//!
//! pub fn App() -> Element {
//! rsx! { CoolName {} } // Renders: "I want a cool name!"
//! }
//! ```
//! ## Implementing a component body deserializer
//! ```rs
//! use syn::{Result, ItemFn, Signature, Ident};
//! use quote::quote;
//!
//! // Create a list of arguments.
//! // If there was no args, just make it empty. The "args" struct is also the deserializer struct.
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
//! // Although unfortunately, the link does not work
//! // Just make sure that your macro is well documented.
//! /// The args and deserializing implementation for the [`name_changer`] macro.
//! #[derive(Clone)]
//! pub struct NameChangerDeserializerArgs<'a> {
//! pub new_name: &'a str,
//! }
//!
//! // Create an output struct.
//! // The ItemFn represents a modified component function.
//! // To read what fields should be here, check out the `DeserializerOutput` struct docs.
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
//! // Just make sure that your macro is well documented.
//! /// The output fields and [`ToTokens`] implementation for the [`name_changer`] macro.
//! #[derive(Clone)]
//! pub struct NameChangerDeserializerOutput {
//! pub comp_fn: ItemFn,
//! }
//!
//! // Implement `ToTokens`, which is forced by `DeserializerOutput`.
//! // This will usually be very simple like this, even for complex deserializers.
//! // That's because of the way the `DeserializerOutput` is designed.
//! impl ToTokens for NameChangerDeserializerOutput {
//! fn to_tokens(&self, tokens: &mut TokenStream) {
//! let comp_fn = &self.comp_fn;
//!
//! tokens.append_all(quote! {
//! #comp_fn
//! });
//! }
//! }
//!
//! impl DeserializerOutput for NameChangerDeserializerOutput {}
//!
//! // Implement `DeserializerArgs`. This is the core part of deserializers.
//! impl<'a> DeserializerArgs<NameChangerDeserializerOutput> for NameChangerDeserializerArgs<'a> {
//! fn to_output(&self, component_body: &ComponentBody) -> Result<NameChangerDeserializerOutput> {
//! let old_fn = &component_body.item_fn;
//! let old_sig = &old_fn.sig;
//!
//! // For more complex uses, you will probably use `quote::parse_quote!` in combination with
//! // creating the structs manually.
//! // However, create the structs manually if you can.
//! // It's more reliable, because you only modify a certain struct field
//! // and set the others to be the clone of the original component body.
//! // That ensures that no information will be accidentally removed.
//! let new_sig = Signature {
//! ident: Ident::new(self.new_name, old_sig.ident.span()),
//! ..old_sig.clone()
//! };
//! let new_fn = ItemFn {
//! sig: new_sig,
//! ..old_fn.clone()
//! };
//!
//! Ok(NameChangerDeserializerOutput {
//! comp_fn: new_fn
//! })
//! }
//! ```
pub mod utils;
pub use utils::DeserializerArgs;
pub use utils::DeserializerOutput;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::*;
/// General struct for parsing a component body.
/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens).
///
/// Refer to the [module documentation](crate::component_body) for more.
pub struct ComponentBody {
/// The component function definition. You can parse this back into a [`ComponentBody`].
/// For example, you might modify it, parse it into a [`ComponentBody`], and deserialize that
/// using some deserializer. This is how deserializers use other deserializers.
///
/// **`item_fn.sig.inputs` includes the context argument!**
/// Keep this in mind when creating deserializers, because you often might want to ignore it.
/// That might be annoying, but it would be bad design for this kind of struct to not be parsable from itself.
pub item_fn: ItemFn,
/// If the function has any arguments other than the context.
pub has_extra_args: bool,
}
impl ComponentBody {
/// Deserializes the body into the [`TOutput`] with the specific [`TArgs`].
/// Even if the args are empty, the [`TArg`] type still determines what [`TOutput`] will be generated.
pub fn deserialize<TOutput, TArgs>(&self, args: TArgs) -> Result<TOutput>
where
TOutput: DeserializerOutput,
TArgs: DeserializerArgs<TOutput>,
{
args.to_output(self)
}
}
impl Parse for ComponentBody {
fn parse(input: ParseStream) -> Result<Self> {
let item_fn: ItemFn = input.parse()?;
let element_type_path = "dioxus_core::Element";
if item_fn.sig.output == ReturnType::Default {
return Err(Error::new(
item_fn.sig.output.span(),
format!("Must return a <{}>", element_type_path),
));
}
let has_extra_args = !item_fn.sig.inputs.is_empty();
Ok(Self {
item_fn,
has_extra_args,
})
}
}

View file

@ -1,36 +0,0 @@
use crate::component_body::ComponentBody;
use quote::ToTokens;
/// The output produced by a deserializer.
///
/// # For implementors
/// Struct field guidelines:
/// * Must be public, so that other deserializers can utilize them.
/// * Should usually be [`Item`]s that you then simply combine in a [`quote!`]
/// in the [`ComponentBodyDeserializer::output_to_token_stream2`] function.
/// * If an [`Item`] might not be included, wrap it in an [`Option`].
/// * Must be at the component function "level"/"context".
/// For example, the [`InlinePropsDeserializer`](crate::component_body_deserializers::inline_props::InlinePropsDeserializer)
/// produces two [`Item`]s; the function but with arguments turned into props, and the props struct.
/// It does not return any [`Item`]s inside the struct or function.
pub trait DeserializerOutput: ToTokens {}
impl<T: ToTokens> DeserializerOutput for T {}
/// The args passed to a [`ComponentBody`] when deserializing it.
///
/// It's also the struct that does the deserializing.
/// It's called "DeserializerArgs", not "Deserializer". Why?
/// Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
/// takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
pub trait DeserializerArgs<TOutput>: Clone
where
TOutput: ToTokens,
{
// There's a lot of Results out there... let's make sure that this is a syn::Result.
// Let's also make sure there's not a warning.
/// Creates a [`ToTokens`] struct from the `self` args and a [`ComponentBody`].
/// The [`ComponentBody::deserialize`] provides a cleaner way of calling this function.
#[allow(unused_qualifications)]
fn to_output(&self, component_body: &ComponentBody) -> syn::Result<TOutput>;
}

View file

@ -1,151 +0,0 @@
use crate::component_body::{ComponentBody, DeserializerArgs};
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
use constcat::concat;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::*;
pub(crate) const COMPONENT_ARG_CASE_CHECK_ERROR: &str = concat!(
"This component does not use PascalCase. \
To ignore this check, pass the \"",
crate::COMPONENT_ARG_CASE_CHECK_OFF,
"\" argument, like so: #[component(",
crate::COMPONENT_ARG_CASE_CHECK_OFF,
")]"
);
const INNER_FN_NAME: &str = "__dx_inner_comp";
fn get_out_comp_fn(orig_comp_fn: &ItemFn) -> ItemFn {
let inner_comp_ident = Ident::new(INNER_FN_NAME, orig_comp_fn.sig.ident.span());
let inner_comp_fn = ItemFn {
sig: Signature {
ident: inner_comp_ident.clone(),
..orig_comp_fn.sig.clone()
},
..orig_comp_fn.clone()
};
let props_ident = match orig_comp_fn.sig.inputs.is_empty() {
true => quote! {},
false => quote! { __props },
};
ItemFn {
block: parse_quote! {
{
#[warn(non_snake_case)]
#[allow(clippy::inline_always)]
#[inline(always)]
#inner_comp_fn
#inner_comp_ident(#props_ident)
}
},
..orig_comp_fn.clone()
}
}
/// The args and deserializing implementation for the [`crate::component`] macro.
#[derive(Clone)]
pub struct ComponentDeserializerArgs {
pub case_check: bool,
}
/// The output fields and [`ToTokens`] implementation for the [`crate::component`] macro.
#[derive(Clone)]
pub struct ComponentDeserializerOutput {
pub comp_fn: ItemFn,
pub props_struct: Option<ItemStruct>,
}
impl ToTokens for ComponentDeserializerOutput {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let comp_fn = &self.comp_fn;
let props_struct = &self.props_struct;
let fn_ident = &comp_fn.sig.ident;
let doc = format!("Properties for the [`{fn_ident}`] component.");
tokens.append_all(quote! {
#[doc = #doc]
#props_struct
#[allow(non_snake_case)]
#comp_fn
});
}
}
impl DeserializerArgs<ComponentDeserializerOutput> for ComponentDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result<ComponentDeserializerOutput> {
let Signature { ident, .. } = &component_body.item_fn.sig;
if self.case_check && !is_pascal_case(&ident.to_string()) {
return Err(Error::new(ident.span(), COMPONENT_ARG_CASE_CHECK_ERROR));
}
if component_body.has_extra_args {
Self::deserialize_with_props(component_body)
} else {
Ok(Self::deserialize_no_props(component_body))
}
}
}
impl ComponentDeserializerArgs {
fn deserialize_no_props(component_body: &ComponentBody) -> ComponentDeserializerOutput {
let ComponentBody { item_fn, .. } = component_body;
let comp_fn = get_out_comp_fn(item_fn);
ComponentDeserializerOutput {
comp_fn,
props_struct: None,
}
}
fn deserialize_with_props(
component_body: &ComponentBody,
) -> Result<ComponentDeserializerOutput> {
let ComponentBody { item_fn, .. } = component_body;
let comp_parsed = match parse2::<ComponentBody>(quote!(#item_fn)) {
Ok(comp_body) => comp_body,
Err(e) => {
return Err(Error::new(
e.span(),
format!(
"This is probably a bug in our code, please report it! Error: {}",
e
),
))
}
};
let inlined_props_output = comp_parsed.deserialize(InlinePropsDeserializerArgs {})?;
let props_struct = inlined_props_output.props_struct;
let props_fn = inlined_props_output.comp_fn;
let comp_fn = get_out_comp_fn(&props_fn);
Ok(ComponentDeserializerOutput {
comp_fn,
props_struct: Some(props_struct),
})
}
}
fn is_pascal_case(input: &str) -> bool {
let mut is_next_lowercase = false;
for c in input.chars() {
let is_upper = c.is_ascii_uppercase();
if (c == '_') || (is_upper && is_next_lowercase) {
return false;
}
is_next_lowercase = is_upper;
}
true
}

View file

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

View file

@ -1,4 +0,0 @@
//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers.
pub mod component;
pub mod inline_props;

View file

@ -2,21 +2,15 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use component::ComponentBody;
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, Path, Token};
use syn::parse_macro_input;
mod component_body;
mod component_body_deserializers;
mod component;
mod props;
mod utils;
// mod rsx;
use crate::component_body::ComponentBody;
use crate::component_body_deserializers::component::ComponentDeserializerArgs;
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
use dioxus_rsx as rsx;
#[proc_macro]
@ -54,6 +48,50 @@ pub fn render(tokens: TokenStream) -> TokenStream {
rsx(tokens)
}
/// Streamlines component creation.
/// This is the recommended way of creating components,
/// though you might want lower-level control with more advanced uses.
///
/// # Arguments
/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names.
/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.**
/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas
/// macros are extremely limited.
///
/// # Features
/// This attribute:
/// * Enforces that your component uses `PascalCase`.
/// No warnings are generated for the `PascalCase`
/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`.
/// Does not disable warnings anywhere else, so if you, for example,
/// accidentally don't use `snake_case`
/// for a variable name in the function, the compiler will still warn you.
/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function.
/// * Verifies the validity of your component.
///
/// # Examples
/// * Without props:
/// ```rust,ignore
/// #[component]
/// fn GreetBob() -> Element {
/// rsx! { "hello, bob" }
/// }
/// ```
///
/// * With props:
/// ```rust,ignore
/// #[component]
/// fn GreetBob(bob: String) -> Element {
/// rsx! { "hello, {bob}" }
/// }
/// ```
#[proc_macro_attribute]
pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
parse_macro_input!(input as ComponentBody)
.into_token_stream()
.into()
}
/// Derive props for a component within the component definition.
///
/// This macro provides a simple transformation from `Scope<{}>` to `Scope<P>`,
@ -82,117 +120,6 @@ pub fn render(tokens: TokenStream) -> TokenStream {
/// ```
#[proc_macro_attribute]
#[deprecated(note = "Use `#[component]` instead.")]
pub fn inline_props(_args: TokenStream, s: TokenStream) -> TokenStream {
let comp_body = parse_macro_input!(s as ComponentBody);
match comp_body.deserialize(InlinePropsDeserializerArgs {}) {
Err(e) => e.to_compile_error().into(),
Ok(output) => output.to_token_stream().into(),
}
}
pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
/// Streamlines component creation.
/// This is the recommended way of creating components,
/// though you might want lower-level control with more advanced uses.
///
/// # Arguments
/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names.
/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.**
/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas
/// macros are extremely limited.
///
/// # Features
/// This attribute:
/// * Enforces that your component uses `PascalCase`.
/// No warnings are generated for the `PascalCase`
/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`.
/// Does not disable warnings anywhere else, so if you, for example,
/// accidentally don't use `snake_case`
/// for a variable name in the function, the compiler will still warn you.
/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function.
/// * Verifies the validity of your component.
/// E.g. if it has a [`Scope`](dioxus_core::Scope) argument.
/// Notes:
/// * This doesn't work 100% of the time, because of macro limitations.
/// * Provides helpful messages if your component is not correct.
/// Possible bugs (please, report these!):
/// * There might be bugs where it incorrectly *denies* validity.
/// This is bad as it means that you can't use the attribute or you have to change the component.
/// * There might be bugs where it incorrectly *confirms* validity.
/// You will still know if the component is invalid once you use it,
/// but the error might be less helpful.
///
/// # Examples
/// * Without props:
/// ```rust,ignore
/// #[component]
/// fn GreetBob() -> Element {
/// rsx! { "hello, bob" }
/// }
///
/// // is equivalent to
///
/// #[allow(non_snake_case)]
/// fn GreetBob() -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp() -> Element {
/// rsx! { "hello, bob" }
/// }
/// // There's no function call overhead since __dx_inner_comp has the #[inline(always)] attribute,
/// // so don't worry about performance.
/// __dx_inner_comp(cx)
/// }
/// ```
/// * With props:
/// ```rust,ignore
/// #[component(no_case_check)]
/// fn GreetPerson(person: String) -> Element {
/// rsx! { "hello, {person}" }
/// }
///
/// // is equivalent to
///
/// #[derive(Props, PartialEq)]
/// #[allow(non_camel_case_types)]
/// struct GreetPersonProps {
/// person: String,
/// }
///
/// #[allow(non_snake_case)]
/// fn GreetPerson(props: GreetPersonProps>) -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp(props: GreetPersonProps>e) -> Element {
/// let GreetPersonProps { person } = props;
/// {
/// rsx! { "hello, {person}" }
/// }
/// }
///
/// __dx_inner_comp(cx)
/// }
/// ```
// TODO: Maybe add an option to input a custom component name through the args.
// I think that's unnecessary, but there might be some scenario where it could be useful.
#[proc_macro_attribute]
pub fn component(args: TokenStream, input: TokenStream) -> TokenStream {
let component_body = parse_macro_input!(input as ComponentBody);
let case_check = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
Err(e) => return e.to_compile_error().into(),
Ok(args) => {
if let Some(first) = args.first() {
!first.is_ident(COMPONENT_ARG_CASE_CHECK_OFF)
} else {
true
}
}
};
match component_body.deserialize(ComponentDeserializerArgs { case_check }) {
Err(e) => e.to_compile_error().into(),
Ok(output) => output.to_token_stream().into(),
}
pub fn inline_props(args: TokenStream, input: TokenStream) -> TokenStream {
component(args, input)
}

View file

@ -1276,7 +1276,8 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let (impl_generics, _, _) = generics.split_for_impl();
let (_, ty_generics, where_clause) = self.generics.split_for_impl();
let (original_impl_generics, ty_generics, where_clause) =
self.generics.split_for_impl();
let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| {
args.insert(
@ -1335,24 +1336,25 @@ Finally, call `.build()` to create the instance of `{name}`.
if self.has_signal_fields() {
let name = Ident::new(&format!("{}WithOwner", name), name.span());
let original_name = &self.name;
let vis = &self.vis;
quote! {
#[doc(hidden)]
#[allow(dead_code, non_camel_case_types, missing_docs)]
#[derive(Clone)]
struct #name #ty_generics {
#vis struct #name #ty_generics {
inner: #original_name #ty_generics,
owner: Owner,
}
impl #impl_generics PartialEq for #name #ty_generics #where_clause {
impl #original_impl_generics PartialEq for #name #ty_generics #where_clause {
fn eq(&self, other: &Self) -> bool {
self.inner.eq(&other.inner)
}
}
impl #impl_generics #name #ty_generics #where_clause {
impl #original_impl_generics #name #ty_generics #where_clause {
/// Create a component from the props.
fn into_vcomponent<M: 'static>(
pub fn into_vcomponent<M: 'static>(
self,
render_fn: impl dioxus_core::prelude::ComponentFunction<#original_name #ty_generics, M>,
component_name: &'static str,
@ -1362,7 +1364,7 @@ Finally, call `.build()` to create the instance of `{name}`.
}
}
impl #impl_generics dioxus_core::prelude::Properties for #name #ty_generics #where_clause {
impl #original_impl_generics dioxus_core::prelude::Properties for #name #ty_generics #where_clause {
type Builder = ();
fn builder() -> Self::Builder {
unreachable!()

View file

@ -1,12 +1,12 @@
{
"name": "dioxus",
"version": "0.0.2",
"version": "0.0.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "dioxus",
"version": "0.0.2",
"version": "0.0.3",
"license": "MIT",
"dependencies": {
"dioxus-ext": "./pkg",

View file

@ -22,10 +22,15 @@ pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
}
#[wasm_bindgen]
pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
pub fn format_selection(
raw: String,
use_tabs: bool,
indent_size: usize,
base_indent: usize,
) -> String {
let block = dioxus_autofmt::fmt_block(
&raw,
0,
base_indent,
IndentOptions::new(
if use_tabs {
IndentType::Tabs

View file

@ -57,14 +57,75 @@ function fmtSelection() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const unformatted = editor.document.getText(editor.selection);
if (editor.document.languageId !== "rust") {
return;
}
if (unformatted.length == 0) {
let end_line = editor.selection.end.line;
// Select full lines of selection
let selection_range = new vscode.Range(
editor.selection.start.line,
0,
end_line,
editor.document.lineAt(end_line).range.end.character
);
let unformatted = editor.document.getText(selection_range);
if (unformatted.trim().length == 0) {
vscode.window.showWarningMessage("Please select rsx invoking this command!");
return;
}
const fileDir = editor.document.fileName.slice(0, editor.document.fileName.lastIndexOf('\\'));
// If number of closing braces is lower than opening braces, expand selection to end of initial block
while ((unformatted.match(/{/g) || []).length > (unformatted.match(/}/g) || []).length && end_line < editor.document.lineCount - 1) {
end_line += 1;
selection_range = new vscode.Range(
editor.selection.start.line,
0,
end_line,
editor.document.lineAt(end_line).range.end.character
);
unformatted = editor.document.getText(selection_range);
}
let tabSize: number;
if (typeof editor.options.tabSize === 'number') {
tabSize = editor.options.tabSize;
} else {
tabSize = 4;
}
let end_above = Math.max(editor.selection.start.line - 1, 0);
let lines_above = editor.document.getText(
new vscode.Range(
0,
0,
end_above,
editor.document.lineAt(end_above).range.end.character
)
);
// Calculate indent for current selection
let base_indentation = (lines_above.match(/{/g) || []).length - (lines_above.match(/}/g) || []).length - 1;
try {
let formatted = dioxus.format_selection(unformatted, !editor.options.insertSpaces, tabSize, base_indentation);
for(let i = 0; i <= base_indentation; i++) {
formatted = (editor.options.insertSpaces ? " ".repeat(tabSize) : "\t") + formatted;
}
if (formatted.length > 0) {
editor.edit(editBuilder => {
editBuilder.replace(selection_range, formatted);
});
}
} catch (error) {
vscode.window.showErrorMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed and you have selected valid rsx with your cursor! \n${error}`);
}
}
@ -82,7 +143,7 @@ function fmtDocumentOnSave(e: vscode.TextDocumentWillSaveEvent) {
function fmtDocument(document: vscode.TextDocument) {
try {
if (document.languageId !== "rust" || document.uri.scheme !== "file") {
if (document.languageId !== "rust") {
return;
}

View file

@ -487,7 +487,7 @@ async fn handle_server_fns_inner(
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
// Location set, then redirect to to Referer
// Location set, then redirect to Referer
if accepts_html {
if let Some(referrer) = referrer {
let has_location = res.headers().get(LOCATION).is_some();

View file

@ -14,8 +14,6 @@ where
let resource = use_resource(move || {
async move {
// this is going to subscribe this resource to any reactivity given to use in the callback
// We're doing this regardless so inputs get tracked, even if we drop the future before polling it
let user_fut = cb.call();
let currently_in_first_run = first_run.cloned();
@ -28,6 +26,10 @@ where
#[cfg(feature = "web")]
if let Some(o) = crate::html_storage::deserialize::take_server_data::<T>() {
// this is going to subscribe this resource to any reactivity given to use in the callback
// We're doing this regardless so inputs get tracked, even if we drop the future before polling it
kick_future(user_fut);
return o;
}
}
@ -60,3 +62,17 @@ where
_ => Some(resource),
}
}
#[inline]
fn kick_future<F, T>(user_fut: F)
where
F: Future<Output = T> + 'static,
{
// Kick the future to subscribe its dependencies
use futures_util::future::FutureExt;
let waker = futures_util::task::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
futures_util::pin_mut!(user_fut);
let _ = user_fut.poll_unpin(&mut cx);
}

View file

@ -11,8 +11,8 @@ use futures_util::{future, pin_mut, FutureExt, StreamExt};
use std::ops::Deref;
use std::{cell::Cell, future::Future, rc::Rc};
/// A memo that resolve to a value asynchronously.
/// Unlike `use_future`, `use_resource` runs on the **server**
/// A memo that resolves to a value asynchronously.
/// Similar to `use_future` but `use_resource` returns a value.
/// See [`Resource`] for more details.
/// ```rust
/// # use dioxus::prelude::*;
@ -34,21 +34,22 @@ use std::{cell::Cell, future::Future, rc::Rc};
/// coordinates: (52.5244, 13.4105)
/// });
///
/// let current_weather = //run a future inside the use_resource hook
/// use_resource(move || async move { get_weather(&country()).await });
///
/// rsx! {
/// //the value of the future can be polled to
/// //conditionally render elements based off if the future
/// //finished (Some(Ok(_)), errored Some(Err(_)),
/// //or is still finishing (None)
/// match current_weather() {
/// Some(Ok(weather)) => rsx! { WeatherElement { weather } },
/// Some(Err(e)) => rsx! { p { "Loading weather failed, {e}" } },
/// None => rsx! { p { "Loading..." } }
/// }
/// }
/// }
/// // Because the resource's future subscribes to `country` by reading it (`country.read()`),
/// // everytime `country` changes the resource's future will run again and thus provide a new value.
/// let current_weather = use_resource(move || async move { get_weather(&country()).await });
///
/// rsx! {
/// // the value of the resource can be polled to
/// // conditionally render elements based off if it's future
/// // finished (Some(Ok(_)), errored Some(Err(_)),
/// // or is still running (None)
/// match current_weather.value() {
/// Some(Ok(weather)) => rsx! { WeatherElement { weather } },
/// Some(Err(e)) => rsx! { p { "Loading weather failed, {e}" } },
/// None => rsx! { p { "Loading..." } }
/// }
/// }
///}
/// ```
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
pub fn use_resource<T, F>(future: impl Fn() -> F + 'static) -> Resource<T>
@ -118,25 +119,25 @@ pub struct Resource<T: 'static> {
callback: UseCallback<Task>,
}
/// A signal that represents the state of a future
/// A signal that represents the state of the resource
// we might add more states (panicked, etc)
#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
pub enum UseResourceState {
/// The future is still running
/// The resource's future is still running
Pending,
/// The future has been forcefully stopped
/// The resource's future has been forcefully stopped
Stopped,
/// The future has been paused, tempoarily
/// The resource's future has been paused, tempoarily
Paused,
/// The future has completed
/// The resource's future has completed
Ready,
}
impl<T> Resource<T> {
/// Restart the future with new dependencies.
/// Restart the resource's future.
///
/// Will not cancel the previous future, but will ignore any values that it
/// generates.
@ -146,19 +147,19 @@ impl<T> Resource<T> {
self.task.set(new_task);
}
/// Forcefully cancel a future
/// Forcefully cancel the resource's future.
pub fn cancel(&mut self) {
self.state.set(UseResourceState::Stopped);
self.task.write().cancel();
}
/// Pause the future
/// Pause the resource's future.
pub fn pause(&mut self) {
self.state.set(UseResourceState::Paused);
self.task.write().pause();
}
/// Resume the future
/// Resume the resource's future.
pub fn resume(&mut self) {
if self.finished() {
return;
@ -168,13 +169,18 @@ impl<T> Resource<T> {
self.task.write().resume();
}
/// Get a handle to the inner task backing this future
/// Clear the resource's value.
pub fn clear(&mut self) {
self.value.write().take();
}
/// Get a handle to the inner task backing this resource
/// Modify the task through this handle will cause inconsistent state
pub fn task(&self) -> Task {
self.task.cloned()
}
/// Is the future currently finished running?
/// Is the resource's future currently finished running?
///
/// Reading this does not subscribe to the future's state
pub fn finished(&self) -> bool {
@ -184,12 +190,12 @@ impl<T> Resource<T> {
)
}
/// Get the current state of the future.
/// Get the current state of the resource's future.
pub fn state(&self) -> ReadOnlySignal<UseResourceState> {
self.state.into()
}
/// Get the current value of the future.
/// Get the current value of the resource's future.
pub fn value(&self) -> ReadOnlySignal<Option<T>> {
self.value.into()
}

View file

@ -0,0 +1,30 @@
//! Signals can degrade into a ReadOnlySignal variant automatically
//! This is done thanks to a conversion by the #[component] macro
use dioxus::prelude::*;
fn main() {
launch(app);
}
fn app() -> Element {
let mut count = use_signal(|| 0);
rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
Child {
count,
"hiiii"
}
}
}
#[component]
fn Child(count: ReadOnlySignal<i32>, children: Element) -> Element {
rsx! {
div { "Count: {count}" }
{children}
}
}

View file

@ -12,7 +12,7 @@ use crate::{CopyValue, Readable, Writable};
/// When a signal calls .read(), it will look for the current ReactiveContext to read from.
/// If it doesn't find it, then it will try and insert a context into the nearest component scope via context api.
///
/// When the ReactiveContext drops, it will remove itself from the the associated contexts attached to signal
/// When the ReactiveContext drops, it will remove itself from the associated contexts attached to signal
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ReactiveContext {
inner: CopyValue<Inner, SyncStorage>,

View file

@ -0,0 +1,184 @@
<p align="center">
<img src="./notes/header.svg">
</p>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/dioxus">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/dioxus/actions">
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!--Awesome -->
<a href="https://dioxuslabs.com/awesome">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<div align="center">
<h3>
<a href="https://dioxuslabs.com"> Website </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
<span> | </span>
<a href="https://dioxuslabs.com/learn/0.4/guide"> Guide </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/zh-cn/README.md"> 中文 </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/pt-br/README.md"> PT-BR </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/ja-jp/README.md"> 日本語 </a>
<span> | </span>
<a href="https://github.com/DioxusLabs/dioxus/blob/master/translations/tr-tr/README.md"> Türkçe </a>
</h3>
</div>
<br/>
> [!WARNING]
> Dioxus 0.5 (şuan 'master' reposunda) uyumluluk kıran değişiklilere sahip ve Dioxus 0.4 ile uyumlu değil.
>Çevirmen Notu (Translator Note): Teknik terimleri orijinal haliyle kullanıp, olabildiğince açıklamaya çalıştım ki hem tecrübeli hem de yeni başlayan arkadaşlar için daha kolay olsun diye. Hatalar varsa affola.
Dioxus, Rust ile geliştirebileceğiniz, uyumlu, performanslı ve ergonomik bir frameworktür (geliştirme yapıları).
```rust
fn app() -> Element {
let mut count = use_signal(|| 0);
rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
}
```
Dioxus web, masaüstü, statik site, mobil uygulama, TUI(Termial User Interface(Terminal Arayüz Uygulamaları)), liveview(canlı görüntü(statik sayfanın gösterilip devamında sunucu ile desteklendiği)) gibi yapılarda uygulamalar geliştirmek için kullanılabilir. Dioxus renderer agnostic (platform bağımsızdır) ve her yerde platform görevi görebilir.
Eğer React biliyorsanız, Dioxus'u zaten biliyorsunuzdur.
## Nevi Şahsına Münhasır Özellikler:
- Masaüstü uygulamaları 10 satır koddan daha az yazılarak doğrudan çalıştırılabilir (Electron kullanmadan).
- İnanılmaz derecede ergonomic ve güçlü durum yönetimine sahiptir.
- Kod dahilinde kapsayıcı döküman - fareyi üzerine getirdiğinizde bütün HTML elementleri, listeners (takipçileri) ve events (olayları) için bilgi edinebilirsiniz.
- Çok hızlı 🔥🔥 ve epey hafıza verimlidir.
- Hot-Reload (Derlemek zorunda olmadan değişikleri görme) sayesinde daha hızlı geliştirme imkanı sağlar.
- Coroutine(Eş zamanlı gibi hissettiren ama aslında sırayla yapılan işlemler) ve suspense(Yükleme bitene kadar halihazırda var olanı gösteren yapı) ile birinci sınıf asenkron desteği sunar.
- Daha fazlası için göz at: [Tam Sürüm Notu](https://dioxuslabs.com/blog/introducing-dioxus/).
## Desteklenen Platformlar
<div align="center">
<table style="width:100%">
<tr>
<td><em>Web</em></td>
<td>
<ul>
<li>WebAssembly ile doğrudan DOM (Sayfayı yenilemeden sayfa içeriği değiştirebilen yapı, aslında sayfanın durumu) manipülasyonu.</li>
<li>SSR(Server Side Rendering (Sunucuda İşlemleri Gerçekleştirme)) ile pre-render (ön işleme) yapıp, client(sayfayı görüntüleyen, sınırlı işlem yapabilen kısım) tarafında sayfanın rehydrate(verilerin güncellenmesi) yapabilme.</li>
<li>Basit bir "Hello World"(Merhaba Dünya) uygulaması 65kb yer kaplar. Bu değer React ile yarışabilecek seviyededir.</li>
<li>Dahili dev server (geliştirme sunucusu(burada 'dx serve' den bahsediliyor)) ve hot-reload ile daha hızlı geliştirme imkanı.</li>
</ul>
</td>
</tr>
<tr>
<td><em>Masaüstü</em></td>
<td>
<ul>
<li>Webview(web temelli render(işleme)) ya da deneysel olarak - WGPU ya da Skia - ile render yapabilirsiniz.</li>
<li>Ayara ihtiyaç duymadan kurulum. Basitçe 'cargo-run' ile uygulamanızı çalıştırabilirsiniz. </li>
<li>Electronsuz, doğrudan sisteme erişim için tam destek. </li>
<li>Linux, macOS ve Windows desteklenir ve küçük çalıştırabilir dosyalar sunar. <3mb binary(derlenmiş çalıştırabilir) </li>
</ul>
</td>
</tr>
<tr>
<td><em>Mobil</em></td>
<td>
<ul>
<li>Webview ya da deneysel olarak - WGPU ya da Skia - ile render.</li>
<li>Android ve iOS desteği. </li>
<li><em>Gözle görülür şekilde</em> React Native'den daha performanslı. </li>
</ul>
</td>
</tr>
<tr>
<td><em>Liveview</em></td>
<td>
<ul>
<li>Tamamen server(sunucu) üzerinde uygulamalarınızı -ya da sadece bir parçasını- render edebilirsiniz. </li>
<li>Axum ve Warp gibi popüler Rust frameworkleri ile entegrasyon.</li>
<li>Çok iyi derecede düşük gecikme sağlar. 10.000+ uygulama eş zamanlı çalışabilir.</li>
</ul>
</td>
</tr>
<tr>
<td><em>Terminal</em></td>
<td>
<ul>
<li>Uygulamaları doğrudan terminalde render edebilirsiniz, örnek:<a href="https://github.com/vadimdemedes/ink"> ink.js</a></li>
<li>Flexbox ve CSS modelleri ile tarayıcıdaki yapılara benzer geliştirme şekilde geliştirme imkanı.</li>
<li>Hazır olarak kullanılabilecek şekilde metin girdi kutuları, butonlar ve odak sistemi.</li>
</ul>
</td>
</tr>
</table>
</div>
## Neden Dioxus?
Uygulama geliştirmek için birsürü seçenek var, neden Dioxusu seçesin ?
Baktığımızda öncelikli olarak Dioxus geliştirici deneyimini önceliğinde tutar. Bu durum Dioxus üzerinde eşsiz birçok özellikte görülür:
- Kendi meta dilimiz (RSX) için oto-format ve [VSCode eklentisi](https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus).
- Hem masaüstü hem de web için hot-reload RSX interpretter(yorumlayıcı(derlemek yerine satır satır kodları çalıştıran)) kullanır.
- İyi dökümantasyona önem veriyoruz. Kod rehberi tamamlandı ve HTML elementleri de dökümente edildi.
- Basitletirmek için çaba gösteriyoruz.
Dioxus ayrıca eklentilere müsait bir platform.
- Basit bir optimize stack-machine() oluşturarak kendi rendererlarınızı kolayca geliştirebilirsiniz.
- Komponentleri ve hatta özel elementleri geliştirip paylaşabilirsiniz.
Yani... Dioxus güzel de, benim neden işime yaramıyor ?
- Gelişimi halen devam etmekte, APIlar değişim göstermekte, bir şeyler bozulabilir.(Bunu yapmamaya çalışsak bile)
- No-std(standart kütüphanesiz) bir ortamda çalıştırmanız gerekiyor.
- React tarzı hook modeli(diğer yapılara ve özelliklere erişmenizi sağlayan) ile arayüz geliştirmeyi sevmiyor olabilirsiniz.
## Katkı (Contributing)
- Websitemizi ziyaret edebilirsiniz. [Katkı kısmı](https://dioxuslabs.com/learn/0.4/contributing).
- Sorunlarınızı raporlayabilirsiniz. [Sorun takipçisi](https://github.com/dioxuslabs/dioxus/issues).
- [Katıl](https://discord.gg/XgGxMSkvUM) Discord'a ve sorularını sor!
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dioxuslabs/dioxus&max=30&columns=10" />
</a>
## Lisans
Bu proje [MIT license] ile lisanslanmıştır.
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Aksi açıkça belirtilmedikçe, yapılan ve Dioxus'a dahil edilen her türlü katkı (contribution) belirtilmesizin MIT lisansı ile lisanslanacaktır.