浏览代码

Merge branch 'upstream' into desktop-hot-reload

Evan Almloff 2 年之前
父节点
当前提交
8a7cee529c

+ 9 - 4
packages/autofmt/src/element.rs

@@ -67,7 +67,8 @@ 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 < 80;
-        let is_small_children = self.is_short_children(children).is_some();
+        let children_len = self.is_short_children(children);
+        let is_small_children = children_len.is_some();
 
         // if we have few attributes and a lot of children, place the attrs on top
         if is_short_attr_list && !is_small_children {
@@ -85,7 +86,11 @@ impl Writer {
 
         // if we have few children and few attributes, make it a one-liner
         if is_short_attr_list && is_small_children {
-            opt_level = ShortOptimization::Oneliner;
+            if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
+                opt_level = ShortOptimization::Oneliner;
+            } else {
+                opt_level = ShortOptimization::PropsOnTop;
+            }
         }
 
         // If there's nothing at all, empty optimization
@@ -360,8 +365,8 @@ impl Writer {
                             Some(len) => total_count += len,
                             None => return None,
                         },
-                        BodyNode::ForLoop(_) => todo!(),
-                        BodyNode::IfChain(_) => todo!(),
+                        BodyNode::ForLoop(_forloop) => return None,
+                        BodyNode::IfChain(_chain) => return None,
                     }
                 }
 

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

@@ -1,16 +1,16 @@
 //! pretty printer for rsx!
 use std::fmt::{Result, Write};
 
+use proc_macro2::Span;
+
 use crate::Writer;
 
 impl Writer {
-    pub fn write_raw_expr(&mut self, exp: &syn::Expr) -> Result {
+    pub fn write_raw_expr(&mut self, placement: Span) -> Result {
         /*
         We want to normalize the expr to the appropriate indent level.
         */
 
-        use syn::spanned::Spanned;
-        let placement = exp.span();
         let start = placement.start();
         let end = placement.end();
 

+ 21 - 16
packages/autofmt/src/lib.rs

@@ -46,6 +46,8 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
     use triple_accel::{levenshtein_search, Match};
 
     for Match { end, start, k } in levenshtein_search(b"rsx! {", contents.as_bytes()) {
+        let open = end;
+
         if k > 1 {
             continue;
         }
@@ -55,7 +57,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
             continue;
         }
 
-        let mut indent_level = {
+        let indent_level = {
             // walk backwards from start until we find a new line
             let mut lines = contents[..start].lines().rev();
             match lines.next() {
@@ -70,31 +72,34 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
             }
         };
 
-        let remaining = &contents[end - 1..];
-        let bracket_end = find_bracket_end(remaining).unwrap();
-        let sub_string = &contents[end..bracket_end + end - 1];
-        last_bracket_end = bracket_end + end - 1;
+        let remaining = &contents[open - 1..];
+        let close = find_bracket_end(remaining).unwrap();
+        // Move the last bracket end to the end of this block to avoid nested blocks
+        last_bracket_end = close + open - 1;
+
+        // Format the substring, doesn't include the outer brackets
+        let substring = &remaining[1..close - 1];
+
+        // make sure to add back whatever weird whitespace there was at the end
+        let mut remaining_whitespace = substring.chars().rev().take_while(|c| *c == ' ').count();
 
-        let mut new = fmt_block(sub_string, indent_level).unwrap();
+        let mut new = fmt_block(substring, indent_level).unwrap();
 
+        // if the new string is not multiline, don't try to adjust the marker ending
+        // We want to trim off any indentation that there might be
         if new.len() <= 80 && !new.contains('\n') {
             new = format!(" {new} ");
-
-            // if the new string is not multiline, don't try to adjust the marker ending
-            // We want to trim off any indentation that there might be
-            indent_level = 0;
+            remaining_whitespace = 0;
         }
 
-        let end_marker = end + bracket_end - indent_level * 4 - 1;
-
-        if new == contents[end..end_marker] {
+        if new == substring {
             continue;
         }
 
         formatted_blocks.push(FormattedBlock {
             formatted: new,
-            start: end,
-            end: end_marker,
+            start: open,
+            end: last_bracket_end - remaining_whitespace - 1,
         });
     }
 
@@ -118,7 +123,7 @@ pub fn write_block_out(body: CallBody) -> Option<String> {
 }
 
 pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
-    let body = syn::parse_str::<dioxus_rsx::CallBody>(block).ok()?;
+    let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
 
     let mut buf = Writer {
         src: block.lines().map(|f| f.to_string()).collect(),

+ 5 - 2
packages/autofmt/src/util.rs

@@ -1,14 +1,17 @@
 pub fn find_bracket_end(contents: &str) -> Option<usize> {
     let mut depth = 0;
 
-    for (i, c) in contents.chars().enumerate() {
+    let mut len = 0;
+
+    for c in contents.chars() {
+        len += c.len_utf8();
         if c == '{' {
             depth += 1;
         } else if c == '}' {
             depth -= 1;
 
             if depth == 0 {
-                return Some(i);
+                return Some(len);
             }
         }
     }

+ 31 - 4
packages/autofmt/src/writer.rs

@@ -1,10 +1,11 @@
-use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed};
+use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, ForLoop};
 use proc_macro2::{LineColumn, Span};
+use quote::ToTokens;
 use std::{
     collections::{HashMap, VecDeque},
     fmt::{Result, Write},
 };
-use syn::{spanned::Spanned, Expr};
+use syn::{spanned::Spanned, Expr, ExprIf};
 
 use crate::buffer::Buffer;
 
@@ -37,8 +38,9 @@ impl Writer {
             BodyNode::Element(el) => self.write_element(el),
             BodyNode::Component(component) => self.write_component(component),
             BodyNode::Text(text) => self.out.write_text(text),
-            BodyNode::RawExpr(exp) => self.write_raw_expr(exp),
-            _ => Ok(()),
+            BodyNode::RawExpr(exp) => self.write_raw_expr(exp.span()),
+            BodyNode::ForLoop(forloop) => self.write_for_loop(forloop),
+            BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain),
         }
     }
 
@@ -176,6 +178,31 @@ impl Writer {
             .or_insert_with(|| prettyplease::unparse_expr(expr))
             .as_str()
     }
+
+    fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result {
+        write!(
+            self.out,
+            "for {} in {} {{",
+            forloop.pat.clone().into_token_stream(),
+            prettyplease::unparse_expr(&forloop.expr)
+        )?;
+
+        if forloop.body.is_empty() {
+            write!(self.out, "}}")?;
+            return Ok(());
+        }
+
+        self.write_body_indented(&forloop.body)?;
+
+        self.out.tabbed_line()?;
+        write!(self.out, "}}")?;
+
+        Ok(())
+    }
+
+    fn write_if_chain(&mut self, ifchain: &ExprIf) -> std::fmt::Result {
+        self.write_raw_expr(ifchain.span())
+    }
 }
 
 trait SpanLength {

+ 35 - 31
packages/autofmt/tests/samples.rs

@@ -1,35 +1,39 @@
 macro_rules! twoway {
-    ($val:literal => $name:ident) => {
-        #[test]
-        fn $name() {
-            let src = include_str!(concat!("./samples/", $val, ".rsx"));
-            let formatted = dioxus_autofmt::fmt_file(src);
-            let out = dioxus_autofmt::apply_formats(src, formatted);
-            pretty_assertions::assert_eq!(&src, &out);
-        }
+    (
+        $(
+
+            // doc attrs
+            $( #[doc = $doc:expr] )*
+            $name:ident
+        ),*
+    ) => {
+        $(
+            $( #[doc = $doc] )*
+            #[test]
+            fn $name() {
+                let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
+                let formatted = dioxus_autofmt::fmt_file(src);
+                let out = dioxus_autofmt::apply_formats(src, formatted);
+                pretty_assertions::assert_eq!(&src, &out);
+            }
+        )*
     };
 }
 
-twoway! ("simple" => simple);
-
-twoway! ("comments" => comments);
-
-twoway! ("attributes" => attributes);
-
-twoway! ("manual_props" => manual_props);
-
-twoway! ("complex" => complex);
-
-twoway! ("tiny" => tiny);
-
-twoway! ("tinynoopt" => tinynoopt);
-
-twoway! ("long" => long);
-
-twoway! ("key" => key);
-
-// Disabled because we can't handle comments on exprs yet
-twoway! ("multirsx" => multirsx);
-
-// Disabled because we can't handle comments on exprs yet
-twoway! ("commentshard" => commentshard);
+twoway![
+    simple,
+    comments,
+    attributes,
+    manual_props,
+    complex,
+    tiny,
+    tinynoopt,
+    long,
+    key,
+    multirsx,
+    commentshard,
+    emoji,
+    messy_indent,
+    long_exprs,
+    ifchain_forloop
+];

+ 5 - 0
packages/autofmt/tests/samples/emoji.rsx

@@ -0,0 +1,5 @@
+rsx! {
+    div { class: "asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd asdasdasd",
+        section { "🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀" }
+    }
+}

+ 13 - 0
packages/autofmt/tests/samples/ifchain_forloop.rsx

@@ -0,0 +1,13 @@
+rsx! {
+    // Does this work?
+    for i in b {
+        // Hey it works?
+        div {}
+    }
+
+    // Some ifchain
+    if a > 10 {
+        //
+        rsx! { div {} }
+    }
+}

+ 17 - 0
packages/autofmt/tests/samples/long_exprs.rsx

@@ -0,0 +1,17 @@
+rsx! {
+    div {
+        div {
+            div {
+                div {
+                    section { class: "body-font overflow-hidden dark:bg-ideblack",
+                        div { class: "container px-6 mx-auto",
+                            div { class: "-my-8 divide-y-2 divide-gray-100",
+                                POSTS.iter().enumerate().map(|(id, post)| rsx! { BlogPostItem { post: post, id: id } })
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 13 - 0
packages/autofmt/tests/samples/messy_indent.rsx

@@ -0,0 +1,13 @@
+fn SaveClipboard(cx: Scope) -> Element {
+    rsx! {
+        div { class: "relative w-1/2 {align} max-w-md leading-8",
+            h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold",
+                "{title}"
+            }
+        }
+    };
+
+    cx.render(rsx! {
+        div { "hello world", "hello world", "hello world" }
+})
+}

+ 3 - 1
packages/autofmt/tests/samples/simple.rsx

@@ -37,7 +37,9 @@ rsx! {
 
     // One level compression
     div {
-        a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" }
+        a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#",
+            "Send invitation"
+        }
     }
 
     // Components

+ 1 - 0
packages/core/Cargo.toml

@@ -41,6 +41,7 @@ serde = { version = "1", features = ["derive"], optional = true }
 tokio = { version = "1", features = ["full"] }
 dioxus = { path = "../dioxus" }
 pretty_assertions = "1.3.0"
+rand = "0.8.5"
 
 [features]
 default = []

+ 305 - 0
packages/core/tests/fuzzing.rs

@@ -0,0 +1,305 @@
+use dioxus::prelude::Props;
+use dioxus_core::*;
+use std::cell::Cell;
+
+fn random_ns() -> Option<&'static str> {
+    let namespace = rand::random::<u8>() % 2;
+    match namespace {
+        0 => None,
+        1 => Some(Box::leak(
+            format!("ns{}", rand::random::<usize>()).into_boxed_str(),
+        )),
+        _ => unreachable!(),
+    }
+}
+
+fn create_random_attribute(attr_idx: &mut usize) -> TemplateAttribute<'static> {
+    match rand::random::<u8>() % 2 {
+        0 => TemplateAttribute::Static {
+            name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
+            value: Box::leak(format!("value{}", rand::random::<usize>()).into_boxed_str()),
+            namespace: random_ns(),
+        },
+        1 => TemplateAttribute::Dynamic {
+            id: {
+                let old_idx = *attr_idx;
+                *attr_idx += 1;
+                old_idx
+            },
+        },
+        _ => unreachable!(),
+    }
+}
+
+fn create_random_template_node(
+    dynamic_node_types: &mut Vec<DynamicNodeType>,
+    template_idx: &mut usize,
+    attr_idx: &mut usize,
+    depth: usize,
+) -> TemplateNode<'static> {
+    match rand::random::<u8>() % 4 {
+        0 => {
+            let attrs = {
+                let attrs: Vec<_> = (0..(rand::random::<usize>() % 10))
+                    .map(|_| create_random_attribute(attr_idx))
+                    .collect();
+                Box::leak(attrs.into_boxed_slice())
+            };
+            TemplateNode::Element {
+                tag: Box::leak(format!("tag{}", rand::random::<usize>()).into_boxed_str()),
+                namespace: random_ns(),
+                attrs,
+                children: {
+                    if depth > 4 {
+                        &[]
+                    } else {
+                        let children: Vec<_> = (0..(rand::random::<usize>() % 3))
+                            .map(|_| {
+                                create_random_template_node(
+                                    dynamic_node_types,
+                                    template_idx,
+                                    attr_idx,
+                                    depth + 1,
+                                )
+                            })
+                            .collect();
+                        Box::leak(children.into_boxed_slice())
+                    }
+                },
+            }
+        }
+        1 => TemplateNode::Text {
+            text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
+        },
+        2 => TemplateNode::DynamicText {
+            id: {
+                let old_idx = *template_idx;
+                *template_idx += 1;
+                dynamic_node_types.push(DynamicNodeType::Text);
+                old_idx
+            },
+        },
+        3 => TemplateNode::Dynamic {
+            id: {
+                let old_idx = *template_idx;
+                *template_idx += 1;
+                dynamic_node_types.push(DynamicNodeType::Other);
+                old_idx
+            },
+        },
+        _ => unreachable!(),
+    }
+}
+
+fn generate_paths(
+    node: &TemplateNode<'static>,
+    current_path: &[u8],
+    node_paths: &mut Vec<Vec<u8>>,
+    attr_paths: &mut Vec<Vec<u8>>,
+) {
+    match node {
+        TemplateNode::Element { children, attrs, .. } => {
+            for attr in *attrs {
+                match attr {
+                    TemplateAttribute::Static { .. } => {}
+                    TemplateAttribute::Dynamic { .. } => {
+                        attr_paths.push(current_path.to_vec());
+                    }
+                }
+            }
+            for (i, child) in children.iter().enumerate() {
+                let mut current_path = current_path.to_vec();
+                current_path.push(i as u8);
+                generate_paths(child, &current_path, node_paths, attr_paths);
+            }
+        }
+        TemplateNode::Text { .. } => {}
+        TemplateNode::DynamicText { .. } => {
+            node_paths.push(current_path.to_vec());
+        }
+        TemplateNode::Dynamic { .. } => {
+            node_paths.push(current_path.to_vec());
+        }
+    }
+}
+
+enum DynamicNodeType {
+    Text,
+    Other,
+}
+
+fn create_random_template(name: &'static str) -> (Template<'static>, Vec<DynamicNodeType>) {
+    let mut dynamic_node_type = Vec::new();
+    let mut template_idx = 0;
+    let mut attr_idx = 0;
+    let roots = (0..(1 + rand::random::<usize>() % 5))
+        .map(|_| {
+            create_random_template_node(&mut dynamic_node_type, &mut template_idx, &mut attr_idx, 0)
+        })
+        .collect::<Vec<_>>();
+    assert!(!roots.is_empty());
+    let roots = Box::leak(roots.into_boxed_slice());
+    let mut node_paths = Vec::new();
+    let mut attr_paths = Vec::new();
+    for (i, root) in roots.iter().enumerate() {
+        generate_paths(root, &[i as u8], &mut node_paths, &mut attr_paths);
+    }
+    let node_paths = Box::leak(
+        node_paths
+            .into_iter()
+            .map(|v| &*Box::leak(v.into_boxed_slice()))
+            .collect::<Vec<_>>()
+            .into_boxed_slice(),
+    );
+    let attr_paths = Box::leak(
+        attr_paths
+            .into_iter()
+            .map(|v| &*Box::leak(v.into_boxed_slice()))
+            .collect::<Vec<_>>()
+            .into_boxed_slice(),
+    );
+    (
+        Template { name, roots, node_paths, attr_paths },
+        dynamic_node_type,
+    )
+}
+
+fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
+    let range = if depth > 3 { 1 } else { 3 };
+    match rand::random::<u8>() % range {
+        0 => DynamicNode::Placeholder(Default::default()),
+        1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
+            key: None,
+            parent: Default::default(),
+            template: Cell::new(Template {
+                name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
+                roots: &[TemplateNode::Dynamic { id: 0 }],
+                node_paths: &[&[0]],
+                attr_paths: &[],
+            }),
+            root_ids: Default::default(),
+            dynamic_nodes: cx.bump().alloc([cx.component(
+                create_random_element,
+                DepthProps { depth, root: false },
+                "create_random_element",
+            )]),
+            dynamic_attrs: &[],
+        })),
+        2 => cx.component(
+            create_random_element,
+            DepthProps { depth, root: false },
+            "create_random_element",
+        ),
+        _ => unreachable!(),
+    }
+}
+
+fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
+    let value = match rand::random::<u8>() % 6 {
+        0 => AttributeValue::Text(Box::leak(
+            format!("{}", rand::random::<usize>()).into_boxed_str(),
+        )),
+        1 => AttributeValue::Float(rand::random()),
+        2 => AttributeValue::Int(rand::random()),
+        3 => AttributeValue::Bool(rand::random()),
+        4 => cx.any_value(rand::random::<usize>()),
+        5 => AttributeValue::None,
+        // Listener(RefCell<Option<ListenerCb<'a>>>),
+        _ => unreachable!(),
+    };
+    Attribute {
+        name: Box::leak(format!("attr{}", rand::random::<usize>()).into_boxed_str()),
+        value,
+        namespace: random_ns(),
+        mounted_element: Default::default(),
+        volatile: rand::random(),
+    }
+}
+
+static mut TEMPLATE_COUNT: usize = 0;
+
+#[derive(PartialEq, Props)]
+struct DepthProps {
+    depth: usize,
+    root: bool,
+}
+
+fn create_random_element(cx: Scope<DepthProps>) -> Element {
+    cx.needs_update();
+    let range = if cx.props.root { 2 } else { 3 };
+    let node = match rand::random::<usize>() % range {
+        0 | 1 => {
+            let (template, dynamic_node_types) = create_random_template(Box::leak(
+                format!(
+                    "{}{}",
+                    concat!(file!(), ":", line!(), ":", column!(), ":"),
+                    {
+                        unsafe {
+                            let old = TEMPLATE_COUNT;
+                            TEMPLATE_COUNT += 1;
+                            old
+                        }
+                    }
+                )
+                .into_boxed_str(),
+            ));
+            println!("{template:#?}");
+            let node = VNode {
+                key: None,
+                parent: None,
+                template: Cell::new(template),
+                root_ids: Default::default(),
+                dynamic_nodes: {
+                    let dynamic_nodes: Vec<_> = dynamic_node_types
+                        .iter()
+                        .map(|ty| match ty {
+                            DynamicNodeType::Text => DynamicNode::Text(VText {
+                                value: Box::leak(
+                                    format!("{}", rand::random::<usize>()).into_boxed_str(),
+                                ),
+                                id: Default::default(),
+                            }),
+                            DynamicNodeType::Other => {
+                                create_random_dynamic_node(cx, cx.props.depth + 1)
+                            }
+                        })
+                        .collect();
+                    cx.bump().alloc(dynamic_nodes)
+                },
+                dynamic_attrs: cx.bump().alloc(
+                    (0..template.attr_paths.len())
+                        .map(|_| create_random_dynamic_attr(cx))
+                        .collect::<Vec<_>>(),
+                ),
+            };
+            Some(node)
+        }
+        _ => None,
+    };
+    println!("{node:#?}");
+    node
+}
+
+// test for panics when creating random nodes and templates
+#[test]
+fn create() {
+    for _ in 0..100 {
+        let mut vdom =
+            VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
+        let _ = vdom.rebuild();
+    }
+}
+
+// test for panics when diffing random nodes
+// This test will change the template every render which is not very realistic, but it helps stress the system
+#[test]
+fn diff() {
+    for _ in 0..10 {
+        let mut vdom =
+            VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
+        let _ = vdom.rebuild();
+        for _ in 0..10 {
+            let _ = vdom.render_immediate();
+        }
+    }
+}

+ 28 - 0
packages/interpreter/src/sledgehammer_bindings.rs

@@ -129,6 +129,34 @@ mod js {
             root.appendChild(els[k]);
         }
     }
+    const bool_attrs = {
+        allowfullscreen: true,
+        allowpaymentrequest: true,
+        async: true,
+        autofocus: true,
+        autoplay: true,
+        checked: true,
+        controls: true,
+        default: true,
+        defer: true,
+        disabled: true,
+        formnovalidate: true,
+        hidden: true,
+        ismap: true,
+        itemscope: true,
+        loop: true,
+        multiple: true,
+        muted: true,
+        nomodule: true,
+        novalidate: true,
+        open: true,
+        playsinline: true,
+        readonly: true,
+        required: true,
+        reversed: true,
+        selected: true,
+        truespeed: true,
+      };
     "#;
 
     extern "C" {

+ 8 - 15
packages/liveview/Cargo.toml

@@ -13,37 +13,29 @@ license = "MIT/Apache-2.0"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
-dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
-dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
-dioxus-hot-reload = { path = "../hot-reload" }
-
-tokio = { version = "1.23.0", features = ["full"] }
+thiserror = "1.0.38"
 futures-util = { version = "0.3.25", default-features = false, features = [
     "sink",
 ] }
 futures-channel = { version = "0.3.25", features = ["sink"] }
-pretty_env_logger = "0.4.0"
+tokio = { version = "1.23.0", features = ["time"] }
 tokio-stream = { version = "0.1.11", features = ["net"] }
-
+tokio-util = { version = "0.7.4", features = ["rt"] }
 serde = { version = "1.0.151", features = ["derive"] }
 serde_json = "1.0.91"
-tokio-util = { version = "0.7.4", features = ["full"] }
-
-interprocess = { version = "1.2.1", optional = true }
+dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
+dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
+dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
+dioxus-hot-reload = { path = "../hot-reload" }
 
 # warp
 warp = { version = "0.3.3", optional = true }
 
 # axum
 axum = { version = "0.6.1", optional = true, features = ["ws"] }
-tower = { version = "0.4.13", optional = true }
 
 # salvo
 salvo = { version = "0.37.7", optional = true, features = ["ws"] }
-thiserror = "1.0.38"
-uuid = { version = "1.2.2", features = ["v4"] }
-anyhow = "1.0.68"
 
 # actix is ... complicated?
 # actix-files = { version = "0.6.2", optional = true }
@@ -51,6 +43,7 @@ anyhow = "1.0.68"
 # actix-ws = { version = "0.2.5", optional = true }
 
 [dev-dependencies]
+pretty_env_logger = { version = "0.4.0" }
 tokio = { version = "1.23.0", features = ["full"] }
 dioxus = { path = "../dioxus", version = "0.3.0" }
 warp = "0.3.3"

+ 31 - 19
packages/rsx/src/node.rs

@@ -95,25 +95,7 @@ impl Parse for BodyNode {
 
         // Transform for loops into into_iter calls
         if stream.peek(Token![for]) {
-            let _f = stream.parse::<Token![for]>()?;
-            let pat = stream.parse::<Pat>()?;
-            let _i = stream.parse::<Token![in]>()?;
-            let expr = stream.parse::<Box<Expr>>()?;
-
-            let body;
-            braced!(body in stream);
-            let mut children = vec![];
-            while !body.is_empty() {
-                children.push(body.parse()?);
-            }
-
-            return Ok(BodyNode::ForLoop(ForLoop {
-                for_token: _f,
-                pat,
-                in_token: _i,
-                expr,
-                body: children,
-            }));
+            return Ok(BodyNode::ForLoop(stream.parse()?));
         }
 
         // Transform unterminated if statements into terminated optional if statements
@@ -221,6 +203,36 @@ pub struct ForLoop {
     pub in_token: Token![in],
     pub expr: Box<Expr>,
     pub body: Vec<BodyNode>,
+    pub brace_token: token::Brace,
+}
+
+impl Parse for ForLoop {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let for_token: Token![for] = input.parse()?;
+
+        let pat = input.parse()?;
+
+        let in_token: Token![in] = input.parse()?;
+        let expr: Expr = input.call(Expr::parse_without_eager_brace)?;
+
+        let content;
+        let brace_token = braced!(content in input);
+
+        let mut children = vec![];
+
+        while !content.is_empty() {
+            children.push(content.parse()?);
+        }
+
+        Ok(Self {
+            for_token,
+            pat,
+            in_token,
+            body: children,
+            expr: Box::new(expr),
+            brace_token,
+        })
+    }
 }
 
 fn is_if_chain_terminated(chain: &ExprIf) -> bool {