Giter VIP home page Giter VIP logo

percy's Introduction

Percy

Actions Status Actions Status

Build frontend browser apps with Rust + WebAssembly. Supports server side rendering.

The Percy Book

This README gives a light introduction to Percy. Check out The Percy Book for a full walk through.

Stable Rust

Percy compiles on stable Rust with one caveat:

On nightly Rust you can create text nodes without quotes.

// Nightly Rust does not require quotes around text nodes.
html! { <div>My text nodes here </div> };

On stable Rust, quotation marks are required.

// Stable Rust requires quotes around text nodes.
html! { <div>{ "My text nodes here " }</div> };

This difference will go away once span locations are stabilized in the Rust compiler - Rust tracking issue.

Getting Started

The best way to get up to speed is by checking out The Percy Book, but here is a very basic example to get your feet wet with.

Quickstart - Getting your feet wet

Percy allows you to create applications that only have server side rendering, only client side rendering, or both server and client side rendering.

Here's a quick-and-easy working example of client side rendering that you can try right now:


First, Create a new project using

cargo new client-side-web-app --lib
cd client-side-web-app

Add the following files to your project.

touch build.sh
touch index.html
touch app.css

Here's the directory structure:

.
├── Cargo.toml
├── build.sh
├── index.html
├── app.css
└── src
    └── lib.rs

Now edit each file with the following contents:

# contents of build.sh

#!/bin/bash

cd "$(dirname "$0")"

mkdir -p public

cargo build --target wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/debug/client_side_web_app.wasm --no-typescript --target web --out-dir ./public --debug
cp index.html public/
cp app.css public/

// contents of src/lib.rs

use wasm_bindgen::prelude::*;
use web_sys;

use percy_dom::prelude::*;

#[wasm_bindgen]
struct App {
  pdom: PercyDom
}

#[wasm_bindgen]
impl App {
    #[wasm_bindgen(constructor)]
    pub fn new () -> App {
        let start_view = html! { <div> Hello </div> };

        let window = web_sys::window().unwrap();
        let document = window.document().unwrap();
        let body = document.body().unwrap();

        let mut pdom = PercyDom::new_append_to_mount(start_view, &body);

        let greetings = "Hello, World!";

        let end_view = html! {
           // Use regular Rust comments within your html
           <div class=["big", "blue"]>
              /* Interpolate values using braces */
              <strong>{ greetings }</strong>

              <button
                class="giant-button"
                onclick=|_event| {
                   web_sys::console::log_1(&"Button Clicked!".into());
                }
              >
                // No need to wrap text in quotation marks (:
                Click me and check your console
              </button>
           </div>
        };

        pdom.update(end_view);

        App { pdom }
    }
}

# contents of Cargo.toml

[package]
name = "client-side-web-app"
version = "0.1.0"
authors = ["Friends of Percy"]
edition = "2018"

[lib]
crate-type = ["cdylib"] # Don't forget this!

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
percy-dom = "0.9"

[dependencies.web-sys]
version = "0.3"
features = [
    "Document",
    "MouseEvent",
    "Window",
    "console"
]

<!-- contents of index.html -->
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" type="text/css" href="app.css"/>
        <title>Client Side Demo</title>
    </head>
    <body style='margin: 0; padding: 0; width: 100%; height: 100%;'>
        <script type="module">
            import init, {App} from '/client_side_web_app.js'
        
            async function run ()  {
                await init('/client_side_web_app_bg.wasm')
                new App()
            }
        
            run()
        </script>
    </body>
</html>

/* contents of app.css */
.big {
  font-size: 30px;
}
.blue {
  color: blue;
}
.giant-button {
  font-size: 24px;
  font-weight: bold;
}

Now run

# Used to compile your Rust code to WebAssembly
cargo install wasm-bindgen-cli

# Or any other static file server that supports the application/wasm mime type
cargo install https

chmod +x ./build.sh
./build.sh

# Visit localhost:8080 in your browser
http ./public --port 8080

And you should see the following:

Client side example

Nice work!

More Examples

API Documentation

Contributing

Always feel very free to open issues and PRs with any questions / thoughts that you have!

Even if it feels basic or simple - if there's a question on your mind that you can't quickly answer yourself then that's a failure in the documentation.

Much more information on how to contribute to the codebase can be found in the contributing section of The Percy Book!

To Test

To run all of the unit, integration and browser tests, grab the dependencies then :

./test.sh

License

MIT

percy's People

Contributors

0xflotus avatar agrif avatar akinaguda avatar alexkehayias avatar alexreg avatar antonhagser avatar chanks avatar chinedufn avatar cryptoquick avatar dbrgn avatar dtolnay avatar finnstokes avatar frisoft avatar hectorj avatar imbolc avatar ivanceras avatar murtyjones avatar nicompte avatar qm3ster avatar qthree avatar rickihastings avatar sepiropht avatar yoshithechinchilla avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

percy's Issues

Unable to run example

Fairly new to Rust so might be missing something simple. Running the example fails with the following errors,

warning: unused manifest key: package.private
warning: unused manifest key: package.private
Compiling itoa v0.4.2
Compiling dtoa v0.4.3
Compiling serde v1.0.70
Compiling syn v0.14.5
error[E0463]: can't find crate for std
|
= note: the wasm32-unknown-unknown target may not be installed

error: aborting due to previous error

For more information about this error, try rustc --explain E0463.
error[E0463]: can't find crate for std
|
= note: the wasm32-unknown-unknown target may not be installed

error: aborting due to previous error

For more information about this error, try rustc --explain E0463.
error: Could not compile dtoa.
warning: build failed, waiting for other jobs to finish...
error: Could not compile itoa.
warning: build failed, waiting for other jobs to finish...
error[E0463]: can't find crate for std
|
= note: the wasm32-unknown-unknown target may not be installed

error: aborting due to previous error

For more information about this error, try rustc --explain E0463.
error: Could not compile serde.
warning: build failed, waiting for other jobs to finish...
error: build failed
warning: unused manifest key: package.private
warning: unused manifest key: package.private
Compiling proc-macro2 v0.4.9
error[E0433]: failed to resolve. Could not find Punct in proc_macro
--> /Users/mobrien/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-0.4.9/src/unstable.rs:131:42
|
131 | let mut op = proc_macro::Punct::new(tt.as_char(), spacing);
| ^^^^^ Could not find Punct in proc_macro

error[E0433]: failed to resolve. Could not find Ident in proc_macro
--> /Users/mobrien/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-0.4.9/src/unstable.rs:468:60
|
468 | Span::Nightly(s) => Ident::Nightly(proc_macro::Ident::new(string, s)),
| ^^^^^ Could not find Ident in proc_macro

error[E0433]: failed to resolve. Could not find Ident in proc_macro
--> /Users/mobrien/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-0.4.9/src/unstable.rs:475:60
|
475 | Span::Nightly(s) => Ident::Nightly(proc_macro::Ident::new_raw(string, s)),
| ^^^^^ Could not find Ident in proc_macro

error[E0412]: cannot find type Ident in module proc_macro
--> /Users/mobrien/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-0.4.9/src/unstable.rs:461:25
|
461 | Nightly(proc_macro::Ident),
| ^^^^^ not found in proc_macro
help: possible candidates are found in other modules, you can import them into scope
|
3 | use Ident;
|
3 | use imp::Ident;
|
3 | use stable::Ident;
|

error[E0412]: cannot find type Ident in module proc_macro
--> /Users/mobrien/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-0.4.9/src/unstable.rs:495:44
|
495 | fn unwrap_nightly(self) -> proc_macro::Ident {
| ^^^^^ not found in proc_macro
help: possible candidates are found in other modules, you can import them into scope
|
3 | use Ident;
|
3 | use imp::Ident;
|
3 | use stable::Ident;
|

error[E0554]: #![feature] may not be used on the stable release channel
--> /Users/mobrien/.cargo/registry/src/github.com-1ecc6299db9ec823/proc-macro2-0.4.9/src/lib.rs:47:34
|
47 | #![cfg_attr(feature = "nightly", feature(proc_macro_raw_ident, proc_macro_span))]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 6 previous errors

Some errors occurred: E0412, E0433, E0554.
For more information about an error, try rustc --explain E0412.
error: Could not compile proc-macro2.

To learn more, run the command again with --verbose.

Example crashes after clicking button

Error summary:

Listening on port 7878
Incoming connection
GET / HTTP/1.1
Host: localhost:7878
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/61.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

/Users/mobrien/projects/percy
FILENAME: ./examples/isomorphic/client/
Incoming connection
GET /bundle.js HTTP/1.1
Host: localhost:7878
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/61.0
Accept: /
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:7878/
Connection: keep-alive

/Users/mobrien/projects/percy
FILENAME: ./examples/isomorphic/client/bundle.js
thread 'main' panicked at 'File not found: Os { code: 2, kind: NotFound, message: "No such file or directory" }', libcore/result.rs:945:5
note: Run with RUST_BACKTRACE=1 for a backtrace.

Define isomorphic

It has a very specific mathematical and chemical meaning, but clearly neither applies here. Most readers will have no clue what you're talking about, so I think it needs definition.

Hard to remember to put commas after attributes / events in `html!`

Right now if you want to use attributes or event handlers you need to end them with a comma:

html! { <div id='my-id', !onclick=|| { count.set(count.get() + 1); },></div> };

This is because the macro system prevents you from directly following an expr with a tt

# Compile time error if you remove the comma from the property
# handling piece of the macro in html_macro.rs
error: `$prop_value:expr` is followed by `$remaining_html:tt`, which is not allowed for `expr` fragments
   --> virtual-dom-rs/src/html_macro.rs:162:102
    |
162 |     ($active_node:ident $root_nodes:ident $prev_tag_type:ident $prop_name:ident = $prop_value:expr $($remaining_html:tt)*) => {
    |                                                                                                      ^^^^^^^^^^^^^^^^^^

I routinely forget to add commas. It's just a weird requirement.

I'm not too versed in procedural macros, but it seems like they might be one way to get around this.

We use a procedural macro for the css! macro but I'd imagine that the html! macro would be quite a bit more complex than that.

If you have thoughts, or experience with proc macros, feel free to leave some notes!

Action Items

These are some rough action items. As we learn more these might change / evolve / be completely off base!

  • Find other procedural macros that could be useful to reference for our html! macro
  • Create an html-macro procedural macro crate in the crates folder
  • Make our html_macro.rs module re-export the html-macro::html procedural macro and make our test suite in html_macro.rs pass (without any commas after events / properties

Unreachable reached on virtual_dom_rs::patch::patch

I'm applying a patch of a diff between the following nodes using the current master version of virtual-dom-rs:

img

Basically it's the change from <span><br></span> to <span>a<br></span>.

Unfortunately the trace only points to the WASM source:

img

Maybe you can reproduce this somehow in a test? I currently don't know how.

(By the way, the debug representation looks quite confusing to me. Maybe it could be made more easy to read?)

Issues with input tag

I had difficulty writing:

<input type="button",>
{"+"}
</input>

however this worked just fine

<div type="button",>
{"+"}
</div>

inside the DOM it seemed to prematurely close the input tag <input type="button"/>+

Proc macro parsing example

As we talked about at Rust Belt Rust -- here is some demo code of a procedural macro that can parse html tags without a comma required after attribute values:

// [package]
// name = "html_macro"
// version = "0.0.0"
// edition = "2018"
//
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = { version = "0.15", features = ["full", "extra-traits"] }
// proc-macro2 = "0.4"

extern crate proc_macro;

use proc_macro2::{TokenStream, TokenTree};
use syn::parse::{Parse, ParseStream, Result};
use syn::{parse_macro_input, Expr, Ident, Token};

#[derive(Debug)]
struct Html {
    // TODO: also support content between the tags
    tags: Vec<Tag>,
}

#[derive(Debug)]
enum Tag {
    /// `<div id="app" class=*CSS>`
    Open {
        name: Ident,
        attrs: Vec<Attr>,
    },
    /// `</div>`
    Close {
        name: Ident,
    },
}

#[derive(Debug)]
struct Attr {
    key: Ident,
    value: Expr,
}

impl Parse for Html {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut tags = Vec::new();

        while !input.is_empty() {
            let tag: Tag = input.parse()?;
            tags.push(tag);
        }

        Ok(Html { tags })
    }
}

impl Parse for Tag {
    fn parse(input: ParseStream) -> Result<Self> {
        input.parse::<Token![<]>()?;
        let optional_close: Option<Token![/]> = input.parse()?;
        let is_close_tag = optional_close.is_some();
        let name: Ident = input.parse()?;

        let mut attrs = Vec::new();
        while input.peek(Ident) && !is_close_tag {
            let key: Ident = input.parse()?;
            input.parse::<Token![=]>()?;

            let mut value_tokens = TokenStream::new();
            loop {
                let tt: TokenTree = input.parse()?;
                value_tokens.extend(Some(tt));

                let peek_end_of_tag = input.peek(Token![>]);
                let peek_start_of_next_attr = input.peek(Ident) && input.peek2(Token![=]);
                if peek_end_of_tag || peek_start_of_next_attr {
                    break;
                }
            }

            let value: Expr = syn::parse2(value_tokens)?;
            attrs.push(Attr { key, value });
        }

        input.parse::<Token![>]>()?;

        if is_close_tag {
            Ok(Tag::Close { name })
        } else {
            Ok(Tag::Open { name, attrs })
        }
    }
}

#[proc_macro]
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let parsed = parse_macro_input!(input as Html);

    // TODO: produce output data structure using `quote!`
    println!("{:#?}", parsed);
    proc_macro::TokenStream::new()
}

Here is the input I was testing with:

#![feature(proc_macro_hygiene)]

use html_macro::html;

fn main() {
    html! { <div id="app" class=*CSS></div> }
}

VElement: props vs attributes

The HTML element attributes in a VElement are stored in a field called props.

Why props and not attrs or attributes? If there's no particular reason behind this I'd suggest a rename to attributes to match the DOM.

(I suspect it was simply copied over from the JS tutorial on which the virtual dom implementation was based on.)

Add more <input> examples

It's straightforward to use <input type="button">, but what about text, radio or checkbox? How to read value on oninput? Is there way to get event and event target inside function attached to oninput?
I placed such code in global callback inside client crate:

app.store.borrow_mut().subscribe(Box::new({
    move || {                
        web_sys::console::log_1(&JsValue::from("Updating state"));
        let s_tag = web_sys::window().unwrap().document().unwrap()
            .get_element_by_id("text_input").unwrap();
        let s_tag: web_sys::HtmlInputElement = s_tag.dyn_into().unwrap();
        let value = s_tag.value();
        *app_store_text_field_rc.borrow_mut() = value;
        global_js.update();
    }
}));

But it looks more like temporary hack, so what's the proper way?

Allow closures with any amount of arguments

Right now if you don't provide an argument to an event callback you see this error

  error[E0593]: closure is expected to take 1 argument, but it takes 0 arguments
    --> examples/browser/src/lib.rs:21:20
     |
  21 |       let end_view = html! {
     |  ____________________^
  22 | |        <div class="big blue">
  23 | |           <strong>Hello, World!</strong>
  24 | |
  ...  |
  27 | |             onclick=|| {
     | |                     -- takes 0 arguments
  ...  |
  33 | |        </div>
  34 | |     };
     | |_____^ expected closure that takes 1 argument
     |
     = note: required for the cast to the object type `dyn std::ops::FnMut(_)`
  help: consider changing the closure to take and ignore the expected argument
     |
  27 |             onclick=|_| {
     |                     ^^^

  error: aborting due to previous error

To fix this we need to use this line

let arg_count = closure.inputs.len();

in order to dynamically generate the number of underscores on this line

Box::new(#value) as Box<FnMut(_)>

Something like this should work:

let underscores: Vec<TokenStream> = (0..arg_count).map(|| { quote! { _ } }).collect();

...

Box::new(#value) as Box<FnMut( #(#underscores),*)>

Along with a unit test right under here that's basically the same thing but with no parameter defined for the closure

#[test]
fn event() {
HtmlMacroTest {
desc: "Events are ignored in non wasm-32 targets",
generated: html! {
<div onclick=|_: u8|{}></div>
},
expected: html! {<div></div>},
}
.test();
}

Benchmarking DOM updates

Benchmarking DOM updates

A pre-requisite for optimizing our virtual dom implementation is having benchmarks in place to let us know where we're starting from and how much progress we're making.

Here's how I think we can benchmark our virtual_dom_rs::patch function that handles updating the real DOM.

This issue is a pre-requisite for some of the potential future optimizations outlined in #10

A potential approach to benchmarking DOM updates

  1. Create a patch-benchmarks cdylib crate that targets WebAssembly

    • Has a PatchBenchmark struct where you specify an old and new virtual-dom, similar to our DiffTestCase struct

    • Each file in this crate has a bench function that returns a -> PatchBenchmark

      fn bench () -> PatchBenchmark {
          PatchBenchmark {
            first: html! { <div> </div> },
            second: html! { <div id="hello",> </div> },
            benchmark_backstory: "What was the thinking behind adding this benchmark ..."
          }
      }
    • build.rs script generates an overall benchmark function that

      • run each of these functions, get the PatchBenchmark
      • Create the first DOM element and patches it with the second DOM element (and vice versa) a bunch of times and console.log's the average time per patch
    • We compile our patch-benchmarks crate to WebAssembly

  2. A JS module imports this WASM overall benchmark function and runs it. We bundle this into patch-benchmark-bundle.js

  3. We spawn a headless chrome instance using Google Chrome Puppeteer, and capture the console.log calls, thus capturing all of our benchmark timings.

  4. Write the benchmark timings to stdout!

Potential future improvements

When we have our first benchmarks in place there will probably be more information that we want to have / a cleaner output format that we can iterate towards.. But step one is just having something in place!

LICENSE file?

The README.md says "MIT" under license, but there's no license file as far as I can see. Without it, it's not clear if this is actually licensed open source.

Also, it would be preferable if you could dual license under MIT/Apache 2.0, as many other Rust crates are. Serde is a good example, if you want one to reference.

CSS-in-Rust and Inline stylesheets (inspired by sheetify)

In a recent project I played around with sheetify, a JavaScript browserify transform that lets you write CSS write next to your view components... and I absolutely loved it.

A big win for me was not needing to think about class names. If I had a some-component-view.js I just made my CSS block for that file someComponentCSS and called it a day. With a code generator this meant zero thinking about where to put or how to name styles.

Another big win was not needing to think about where / how to organize styles. When the style is just next to the component there is no "what CSS file to I put this in?" question to answer.

// rough example in JS land... some-component-view.js

var someComponentCSS = css`
:host {
  background-color: red;
}
`

...

return html`<div class='${css}></div>`

// ...

Of course there are downsides to inline stylesheets. The first that comes to mind is that it is much less popular than sass/css so less people know it so it's non-standard.

But, support for inline stylesheets in Percy would be completely optional. If you don't want them you can still just do

html! { <div class='my-class',></div> }

With that background out of the way... here's how I think we can do inline stylesheets in Rust:

Structure of VirtualNode

Hi. I started a refactoring of the VirtualNode struct.

In the DOM, we mostly deal with elements, but there are also nodes. The relation is that an element is a node, but not the other way around.

You probably know this already, but just for clarification: A node can have the following node types:

  • ELEMENT_NODE
  • TEXT_NODE
  • COMMENT_NODE
  • ..and a few others.

See this for the full list.

Right now the VirtualNode handles both text nodes and elements. A text node is a VirtualNode with a non-empty text field, and an element node is a VirtualNode with an empty text field.

This is both a bit ugly and causes confusion. It also makes it possible to represent illegal states (e.g. a text node with children). Here's one of my favorite talks about this topic.

I started converting the VirtualNode type to this definition:

#[derive(Debug, PartialEq)]
pub enum VirtualNode {
    /// An element node (node type `ELEMENT_NODE`).
    Element(VirtualNodeElement),
    /// A text node (node type `TEXT_NODE`).
    Text(VirtualNodeText),
}

This has mostly worked fine, but I'm a bit unsure about the DomUpdater part. The root_node in the DomUpdater is a web_sys::Element. This makes it impossible to use text nodes as root element.

Is this intended, do we want the root node to always be an element node? Or should I convert the DomUpdater to use a web_sys::Node as root?

Also, are you aware of an enum or trait that covers all web_sys subtypes of Node, like Element or Text?

Unexpected element nesting

Noticing issues where elements will be nested in previous elements

<div>
    <section>{"foo"}</section>
   <section>{"bar"}</section>
</div>

will result in:

<div>
   <section>
       foo
       <section>bar</section>
   </section>
</div>

Routing

Any ideas about routing?
It is an essential part, and strongly informs how we work with the server since we are aiming for isomorphicity.

Deny missing docs

Noticing that a bunch of things don't have documentation so need to add deny missing docs and add some docs.

Allow for self closing tags such as `<br>` and `<br />`

While trying to come up with a test for another bug in the DOM diffing, I came across this:

#[wasm_bindgen_test]
fn bug() {
    DiffPatchTest {
        desc: "Diff/patch bug",
        old: html! { <div id="x",>{"a"} <br> {"b"} <br> {"c"}</div> },
        new: html! { <div id="x",>{"a"} <br> {"b"} <br> {"c"}</div> },
        override_expected: None,
    }
    .test();
}

This should result in no changes, but triggers this panic:

---- diff_patch::bug output ----
    error output:
        panicked at 'called `Result::unwrap()` on an `Err` value: RefCell { value: VirtualNode | tag: div, props: {
            "id": "x"
        }, text: None, children: Some(
            [
                RefCell {
                    value: VirtualNode | tag: , props: {}, text: Some(
                        "a"
                    ), children: Some(
                        []
                    ) |
                },
                RefCell {
                    value: VirtualNode | tag: br, props: {}, text: None, children: Some(
                        [
                            RefCell {
                                value: VirtualNode | tag: , props: {}, text: Some(
                                    "b"
                                ), children: Some(
                                    []
                                ) |
                            },
                            RefCell {
                                value: VirtualNode | tag: br, props: {}, text: None, children: Some(
                                    [
                                        RefCell {
                                            value: VirtualNode | tag: , props: {}, text: Some(
                                                "c"
                                            ), children: Some(
                                                []
                                            ) |
                                        }
                                    ]
                                ) |
                            }
                        ]
                    ) |
                }
            ]
        ) | }', libcore/result.rs:1009:5

    JS exception that was thrown:
        RuntimeError: unreachable
            at __rust_start_panic (wasm-function[4398]:33)
            at rust_panic (wasm-function[4376]:31)
            at std::panicking::rust_panic_with_hook::h7516bd59511f8270 (wasm-function[4371]:305)
            at std::panicking::continue_panic_fmt::h30f25cfab802818a (wasm-function[4370]:120)
            at rust_begin_unwind (wasm-function[4369]:3)
            at core::panicking::panic_fmt::h532e7fae5a9b5860 (wasm-function[4437]:70)
            at core::result::unwrap_failed::h9ce052480df8a851 (wasm-function[2326]:340)
            at _$LT$core..result..Result$LT$T$C$$u20$E$GT$$GT$::unwrap::h5597e83cae484e77 (wasm-function[2325]:175)
            at diff_patch::bug::hb9d7d6e92f358366 (wasm-function[376]:11500)
            at core::ops::function::FnOnce::call_once::h9a0c5ff60106b3b9 (wasm-function[76]:22)

Not sure if it's due to wrong use of the line breaks in the HTML macro, but it could be the reason for the bugs I'm seeing (I also have some "unreachable executed" exceptions in my JS code from the WASM binary).

UTF16 Text

JavaScript uses UTF16-encoding for strings, while Rust uses UTF8.

The value of VirtualNode::Text is String, so for every DOM update the text needs to be re-encoded.

I wonder if it might make sense to have a VirtualNode::TextUtf16(Vec<u16>) variant for when you already deal with UTF16 text internally? Might give some performance benefits, although I'm not sure how much.

Ownership of virtual_dom_rs::patch params

The virtual_dom_rs::patch function takes the root_node element by value, not by mutable reference. This means that the element cannot be re-used after patching it.

Is this a conscious design decision? What would be the reason for it?

Crate names

Great project, but:

Why do all the crate names have "rs" in them?

That's very unidiomatic.

Comparison to Yew

Hi!

What is your vision for Percy with respect to how it will compare to Yew?

Thanks!

Dave

Replace `percy-webapis` with `web-sys`

Now that web-sys has matured we shouldn't be rolling our own web apis. We'll need to add the web-sys dependency

[dependencies.web-sys]
git = "https://github.com/rustwasm/wasm-bindgen"
features = [
    "Window",
    # ...
]

And then one by one replace our percy_webapis crate usages with web-sys

Whitespace in HTML macro

How do I create I the equivalent of Hello <img src="#" alt="Big"> World with the html macro?

When I do html! { <div> Hello <img src="#" alt="Big"> World </div> } then the whitespace around the text is stripped. Can it somehow be escaped without using the text! macro and variable replacement?

The example app crashes

How to reproduce:

  1. Go to https://percy-isomorphic.now.sh/?init=42
  2. Click on "Contributors"
  3. Click on "Isomorphic web app"
  4. Click on the "Click me!" button twice.

Result:

I get this error:

Uncaught RuntimeError: memory access out of bounds
    at <alloc::rc::Rc<T> as core::ops::drop::Drop>::drop::ha4cc7db8eac72ced (wasm-function[135]:32)
    at core::ptr::real_drop_in_place::h13b8aa8efd871d72 (wasm-function[120]:3)
    at <(dyn core::ops::function::FnMut(A) .> R + 'static) as wasm_bindgen::closure::WasmClosure>::describe::destroy::h1c9def9cc2c875ba (wasm-function[116]:10)
    at Function.cb (https://percy-isomorphic.now.sh/isomorphic_client.js:367:34)

This happens in both Chrome and Firefox.

CSS-in-rs

I don't understand the purpose of the current behavior of the css! macro.
Why is CSS being written to an external file, regardless of if the component will ever be displayed or not?
I feel like having the component own its css makes more sense than a macro writing files during compilation. This way, critical CSS can trivially be inlined during SSR.

Using strings for CSS in Rust is a poor experience. We should use tokens instead

So I'm using the css_rs_macro while building a real site and here's one the inconveniences that I've run into so far

Needing the CSS to be an &'static str is a poor experience for a number of reasons.

  1. You can't use C style // comments in your CSS since they don't work like you'd expect
  2. I had a background color that I wanted to re-use. Using CSS variables felt very stringly typed, especially when Rust has a great type system.

Did some reading through syn's documentation and there's a ton that you can do with procedural macros. Seems like they would let us be able to ditch this:

static HOMEPAGE_HERO_CSS: &'static str = css! {"
:host {
    height: 600px;
    padding-left: 20px;
    padding-right: 20px;
    padding-top: 30px;
}
"};

in favor of:

static HOMEPAGE_HERO_CSS: &'static str = css! {
:host {
    height: 600px;
    padding-left: 20px;
    padding-right: 20px;
    padding-top: 30px;
}
};

And ideally we could something along the lines of (not yet sure if it's possible...)

pub static RE_USABLE_COLOR: &'static str = "magenta"l

static HOMEPAGE_HERO_CSS: &'static str = css! {
:host {
    // Use other variables from our macro
    background-color: *RE_USABLE_COLOR
}
};

Access to Events in handlers (virtual DOM)

Unless I'm mistaken, there isn't currently any way to access the event itself inside event handlers.

It looks like some support for this was added recently for oninput, and I think that route is maybe a bit labor intensive but worth pursuing.

However, in the meantime, would it be possible to let generic handlers access their event? Either as a Event struct, or an even more generic JsValue. This maybe isn't a very pleasant API, but it would be a quick win, and let us actually inspect the events for things like keycodes that aren't available any other way.

use of undeclared type or module `Rc`

Hey Chinedu, just going through the example and got this:

user:client$ ./build.sh --verbose
  
  [1/10] Checking `rustc` version...
  [2/10] Checking crate configuration...
  [3/10] Adding WASM target...
  info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date
  [4/10] Compiling to WASM...
     Compiling client v0.1.0 (/path/to/client)
  error[E0433]: failed to resolve: use of undeclared type or module `Rc`
    --> src/lib.rs:19:20
     |
  19 |       let end_view = html!{
     |  ____________________^
  20 | |        <div class="big blue">
  21 | |           <strong>Hello, World!</strong>
  22 | |
  ...  |
  31 | |        </div>
  32 | |     };
     | |_____^ use of undeclared type or module `Rc`
  
  error: aborting due to previous error
  
  For more information about this error, try `rustc --explain E0433`.
  error: Could not compile `client`.
  
| To learn more, run the command again with --verbose.
Error: Compiling your crate to WebAssembly failed
Caused by: failed to execute `cargo build`: exited with exit code: 101

Here are the versions for my system:

user:client$ rustup --version
rustup 1.16.0 (beab5ac2b 2018-12-06)

user:client$ cargo --version
cargo 1.34.0-nightly (245818076 2019-01-27)

user:client$ rustc --version
rustc 1.34.0-nightly (4b1e39b7b 2019-02-05)

DOM patch (TruncateChildren) not working properly

The TruncateChildren patch does not always seem to work properly when dealing with text nodes.

Test to trigger the problem:

diff --git a/crates/virtual-dom-rs/tests/diff_patch.rs b/crates/virtual-dom-rs/tests/diff_patch.rs
index d961d15..e44bc06 100644
--- a/crates/virtual-dom-rs/tests/diff_patch.rs
+++ b/crates/virtual-dom-rs/tests/diff_patch.rs
@@ -10,6 +10,10 @@ extern crate virtual_dom_rs;
 
 wasm_bindgen_test_configure!(run_in_browser);
 
+/// A diff and patch test.
+///
+/// Note: Make sure that both the old and new DOM element contain a root node
+/// with the `id` attribute set!
 struct DiffPatchTest {
     old: VirtualNode,
     new: VirtualNode,
@@ -51,6 +55,25 @@ fn truncate_children() {
     .test();
 }
 
+#[wasm_bindgen_test]
+fn truncate_children_2() {
+    DiffPatchTest {
+        old: html! {
+         <div id="old2",>
+           {"ab"} <p></p> {"c"}
+         </div>
+        },
+        new: html! {
+         <div id="new2",>
+           {"ab"} <p></p>
+         </div>
+        },
+        desc: "Truncates extra children",
+        override_expected: None,
+    }
+    .test();
+}
+
 #[wasm_bindgen_test]
 fn remove_attributes() {
     DiffPatchTest {

This test fails:

    error output:
        panicked at 'assertion failed: `(left == right)`
          left: `"<div id=\"new2\">ab<p></p>c</div>"`,
         right: `"<div id=\"new2\">ab<p></p></div>"`: Truncates extra children', crates/virtual-dom-rs/tests/diff_patch.rs:194:9

(When logging the patches, the generated patchset is [AddAttributes(0, {"id": "new2"}), TruncateChildren(0, 2)] which looks correct to me.)

(Btw, I noticed that using the same ID in two tests breaks them. It would be good if tests would be "hygienic" in the sense that tests don't influence each other. Or at least document that behavior.)

unresolved import `virtual_dom_rs::webapis

Using

I got the following error:

x@msi:/d/percy$ ./examples/isomorphic/start.sh
warning: unused manifest key: package.private
warning: unused manifest key: package.private
   Compiling isomorphic-app v0.1.0 (file:///d/percy/examples/isomorphic/app)    
error[E0432]: unresolved import `virtual_dom_rs::webapis`
  --> examples/isomorphic/app/src/lib.rs:17:25
   |
17 | pub use virtual_dom_rs::webapis::*;
   |                         ^^^^^^^ Could not find `webapis` in `virtual_dom_rs`

error[E0412]: cannot find type `Element` in this scope
  --> examples/isomorphic/app/src/lib.rs:59:46
   |
59 |     pub fn update_dom(&mut self, root_node: &Element) {
   |                                              ^^^^^^^ not found in this scope

warning: unused import: `std::cell::Cell`
 --> examples/isomorphic/app/src/lib.rs:9:5
  |
9 | use std::cell::Cell;
  |     ^^^^^^^^^^^^^^^
  |
  = note: #[warn(unused_imports)] on by default

warning: unused import: `std::cell::RefCell`
 --> examples/isomorphic/app/src/state.rs:7:5
  |
7 | use std::cell::RefCell;
  |     ^^^^^^^^^^^^^^^^^^

error: aborting due to 2 previous errors

Some errors occurred: E0412, E0432.
For more information about an error, try `rustc --explain E0412`.
error: Could not compile `isomorphic-app`.

To learn more, run the command again with --verbose.
warning: unused manifest key: package.private
warning: unused manifest key: package.private
   Compiling isomorphic-app v0.1.0 (file:///d/percy/examples/isomorphic/app)    
error[E0432]: unresolved import `virtual_dom_rs::webapis`
  --> examples/isomorphic/app/src/lib.rs:17:25
   |
17 | pub use virtual_dom_rs::webapis::*;
   |                         ^^^^^^^ Could not find `webapis` in `virtual_dom_rs`

error[E0412]: cannot find type `Element` in this scope
  --> examples/isomorphic/app/src/lib.rs:59:46
   |
59 |     pub fn update_dom(&mut self, root_node: &Element) {
   |                                              ^^^^^^^ not found in this scope

warning: unused import: `std::cell::Cell`
 --> examples/isomorphic/app/src/lib.rs:9:5
  |
9 | use std::cell::Cell;
  |     ^^^^^^^^^^^^^^^
  |
  = note: #[warn(unused_imports)] on by default

warning: unused import: `std::cell::RefCell`
 --> examples/isomorphic/app/src/state.rs:7:5
  |
7 | use std::cell::RefCell;
  |     ^^^^^^^^^^^^^^^^^^

error: aborting due to 2 previous errors

Some errors occurred: E0412, E0432.
For more information about an error, try `rustc --explain E0412`.
error: Could not compile `isomorphic-app`.

To learn more, run the command again with --verbose.

Force quoting punctuation

Problem

Right now if you write something like

html! { Hello, what. is- your/ name? };

You'll end up with:

"Hello , what . is - your / name ?"

This is because while parsing a TokenStream punctuation is treated as it's own token and you can't know whether or not there was space before/after it.

Potential Solution

If we see any punctuation while parsing text

let is_comma = input.peek(Token![,]);

Alert the user with a compiler_error! that they should do this

html! { "Hello, what. is- your/ name?" };

Right now in that linked code we try to assume how the punctuation should be spaced, but a compiler error is a bit better since we can't reliably guess correctly for every type of punctuation.


Blocker

html! { "Hello, what. is- your/ name?" };

Isn't even possible right now. So we need the parser to support converting literals into text nodes. So more or less just peeking and if we see a literal we parse it into Tag::text.. or something like that.

Can we do without 'diffing'?

This is not an 'issue' but a question about a particular approach.

Let me start by saying that I'm a complete newb on Rust/Wasm and am just looking around to see how this new eco system works and whether I can use it on a project that I'm considering.

As part of this research I first came across Yew and then following various rabbit holes found Percy. So far Percy's approach to me seems easier however there is one part of Yew which I thought was particularly good. Yew has a concept of 'ShoudRender' for each component which when set to 'true' renders that component.

I was wondering whether you had considered this approach and what reasons you had for rejecting it, because that takes away the whole need to do any 'diffing' and leaves the responsibility firmly in the user's lap for when a component should be (re)rendered.

Make wasm-bindgen an optional dependency

When you’re on the server side you’re likely rendering your application to an HTML String and don’t need any of the diffing or wasm-bindgen powered patching.

Therefore - wasm-bindgen should be an optional dependency behind a feature that people will typically only use when targeting WebAssembly.

I’m thinking that it should be in the default-features = [“wasm-bindgen”] just to be that much easier for new people to get up and running without needing to worry about features.

Then on the server side people can disable it by adding default-features = false.

Create a base docker image for our example server

Make the Dockerfile in the root directory that we use for now.sh deployment of our isomorphic-server example depend on a Dockerfile that we publish to docker hub.

#32 (comment)


Waiting until our Dockerfile stabilizes a bit as we've been changing it frequently lately and that's easier to do in one repo.. But after that we can set up automatic travis deploys.. Maybe all master branch builds in the dockerfile repo would be tagged with the $TRAVIS_BUILD_NUMBER and latest

virtual-dom-rs bug: "Node's should only receive change text patches"

Either diff or patch in virtual-dom-rs has bug. Probably something related to node indexing. It panics on tag&text change. Here is code (added to isomorphic example home view), that causes panic:

<table>
{
    (0..click_count.parse().unwrap()).into_iter().rev().map(|i| {
        if i%2 == 0 {
            html!{
                <tr><th>{i.to_string()}</th></tr>
            }
        } else {
            html!{
                <tr><td>{i.to_string()}</td></tr>
            }
        }
        
    }).collect::<Vec<_>>()
}
</table>

I also made fork with this code, for convenience: https://github.com/qthree/percy/tree/patch_bug
With this standard example if you open http://localhost:7878/?init=2 and start clicking "Click me", it will insert tags to the wrong parent. And if open http://localhost:7878/?init=4 then it will panic on the first click.

Decide on browser support

What is the minimum client browser this project targets for the current version?
Is extreme fallback, such as server side session for total <noscript> in scope?
Or is this meant to be a distinctly clientside framework, just with an SSR feature?

Improved CSS workflows

I've been following this project for a month or so and plan to start building an actual app soon enough. There's a few things that have been bothering me with the CSS workflow however, I feel that it is just a little bit too magical and inflexible:

  • In issue #27 it was discussed that there needs to be a way for CSS to be output as the programmer desires, and not necessarily as a single file.
  • In issue #17 a feature was requested to have inline CSS support directly in Rust using a macro.

I've started working on is a new crate that should support CSS Modules, you use it like this:

let css = include_css_module!("relative-path-to-file.css");

// Get an aliased version of .yourClassName that will not conflict:
css.get("yourClassName");

// Get the stylesheet containing aliased classes:
css.stylesheet;

I'd like to help resolve the issues here too. My next step will be to implement a css_module macro that allows defining CSS inline. I'd be interested in hearing any thoughts about what I'm working on, and perhaps what else I should include.

Diffing algorithm & vdom stories

Hey @chinedufn :D Here's feedback as requested. It's mostly about what we got wrong in Choo's diffing, and what we would do differently if we had the chance to start from scratch. Hope it comes in useful for Percy's development!

Diffing

An optimization that we were planning for nanomorph, but never realized was to move to a well-known diffing algorithm rather than an ad-hoc hacky one.

After a couple of years of trial-and-error engineering, it became pretty clear to us that there's 2 important features that are hard to get right through trial-and-error:

  1. always creating the smallest possible diff, which means the best performance the moment you modify the DOM.
  2. being able to move elements around in the DOM. (e.g. changing a sequence of sibling nodes from [1, 2, 3, 4] to [1, 3, 2, 4]).
  3. achieving the two features above in at-worst linear time O(n). (we used to do backtracking during diffing, and yep nope never again).

If I had to implement a diffing algorithm today, I'd probably start by looking at the Myers diffing algorithm. It's a pretty straight forward algorithm to achieve small diffs. Once that works, the patience-diff algorithm is an optimization on top of Myers that allows for smaller, cleaner diffs.

note: an incomplete patience-diff implementation in JS can be found here. Rust should be a much better fit for a similar approach because it actually has abstractions for binary opcodes. This is also similar-ish to the way ember does it in Glimmer.

Server Rendering

A cool thing we never implemented was async rendering. Being able to wait for requests to complete while rendering turned out to be a must-have to scale out our server rendering pipeline. We never ended up implementing this in Choo, but that was more due to time constraints rather than anything else.

As a note: I wouldn't go as far as to split all rendering using requestIdleCallback() (RIC) the way for example React does it. I think RIC has a place, but it's a much better choice to manually use it in performance-critical paths, rather than using it by default through a scheduler. The main drawback of always-on RIC is that it creates compatibility problems with third party programs that have their own scheduling loops (e.g. maps, widgets, glsl frames, etc.)

In practice in Rust this would mean every template would return a Future. This is probably not ready for prime-time yet; and most likely not quite needed right now. But I figured I'd share what we ended running into, and the insight we had.

Standardization Efforts

Something to be aware of is that browser vendors are working on standardizing (a part of) DOM diffing (proposal). The webcomponent folks seem to be pushing this effort.

Personally I think this idea is rather promising as it opens up the potential for better interop between (JS) modules. But at least this spec doesn't seem to be exposing anything high-level, so in any case it would require wrappers to at least be useful.

Conclusion

Hope this comes in useful for the future! I'd love to see Percy realize a better diffing engine than we ever could with Choo! :D

Further Reading

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.