-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
447 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
--- | ||
title: How it works | ||
|
||
--- | ||
|
||
|
||
The interactivity layer is a very lightweight layer over the leptos `reactive_graph` signals crate. | ||
|
||
The hydration strategy is loosely borrowed from quik. Html serialization and lazy event patching allow for html-rust splitting and zero pre-hydration events missed. | ||
|
||
The opinions on client-server relationship, routing are very astro, as is the integrations layer for crates like axum, leptos or bevy. | ||
|
||
## The Preprocessor | ||
|
||
The sweet preprocessor is a binary downloaded via `cargo binstall sweet-cli` that watches rust files and extracts the html with some metadata. | ||
|
||
|
||
I havent come across this in the rust ecosystem before so its probably worth clarifying what i mean by a rust preprosser. | ||
|
||
## What it does | ||
|
||
The preprocessor literally splits rsx components into a html and a rust file, and only recompiles if the rust file hash changes. | ||
|
||
stores the rsx block locations as html attributes and lazily hooks them up to the wasm code. | ||
|
||
Naturally any changes outside of the | ||
|
||
```rust | ||
|
||
let (value,set_value) = signal(); | ||
|
||
rsx!{ | ||
<div>the {val}th value is {val}</div> | ||
<button onclick={|e|val += 1}>increment</button> | ||
} | ||
``` | ||
Will be serialized by as this html | ||
```html | ||
<button onclick="_sweet.event(0,event)">increment</button> | ||
<div data-sid="0" data-sblock="4,6,18">the 11th value is 11</div> | ||
``` | ||
And this rust | ||
```rust | ||
Hydrated { | ||
events: vec![Box::new(handle_click)], | ||
blocks: vec![ | ||
HydratedBlock { | ||
node_id: 0, | ||
part_index: 1, | ||
}, | ||
HydratedBlock { | ||
node_id: 0, | ||
part_index: 3, | ||
}, | ||
], | ||
} | ||
``` | ||
|
||
|
||
These four numbers are all thats required |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
--- | ||
# for parser whatever it is for the rest of that line, trimming whitespace, as a string. people can parse it as they wish later. | ||
title: Sweet | ||
|
||
|
||
--- | ||
|
||
A web framework building on the best ideas from astro, quik and more, all entirely in a rust ecosystem. | ||
|
||
- 🔥 **Smokin hot reload** sweet splits html from rust and only recompiles code changes. Macros are *preprocessed*, speeding up compile times? ⚠️bench this⚠️ | ||
- 🌊 **Stay Hydrated** sweet collects pre-hydration events and plays them back in order. | ||
- 🌐 **No signal, no problem** sweet provides event and signal primitives, and easily integrates other frameworks astro-style. | ||
- 🦀 **Rusted to the core** components are described as *regular structs and traits*. | ||
- 🧪 **A full ecosystem** sweet has a built-in component library and testing framework, as well as integrations with axum, leptos and bevy. | ||
|
||
## Choose your own adventure | ||
- [Quick Start - Counter](./quickstart.md) | ||
- [How it works](./how-it-works.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
title: Using Markdown | ||
|
||
--- | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
--- | ||
title: Quickstart | ||
|
||
sidebar: | ||
oder: 1 | ||
--- | ||
|
||
A counter is a great way to demonstrate core concepts, for a deeper dive read [how it works](./how-it-works) | ||
|
||
<details> | ||
<summary>Show final code</summary> | ||
</details | ||
Assuming rust nightly is installed, the following will get a new project up and running in four steps. | ||
1. We'll start by creating a new project and adding sweet. | ||
```sh | ||
cargo init hello-sweet | ||
cd hello-sweet | ||
cargo add sweet | ||
``` | ||
2. Sweet uses file-based routes, lets create our index page. | ||
```rust src/pages/index.rs | ||
use sweet::prelude::*; | ||
struct Index; | ||
impl Route for Index { | ||
fn rsx(self) -> Rsx { | ||
rsx!{ | ||
<div>hello world!</div> | ||
} | ||
} | ||
} | ||
``` | ||
3. Now lets run the server in the main function. | ||
```rust src/main.rs | ||
use sweet::prelude::*; | ||
fn main(){ | ||
SweetServer::default().run(); | ||
} | ||
``` | ||
4. Finally we'll run the sweet preprocessor and run our server. | ||
```sh | ||
cargo binstall sweet-cli | ||
sweet parse | ||
cargo run | ||
# server running at htp://127.0.0.1:3000 | ||
``` | ||
|
||
Visiting this page we get a heartwarming greeting, but now its time to make it iteractive. Lets create a counter component, a struct with the fields used as rsx props. A component is similar to a route but cannot use Axum extractors as props. | ||
|
||
```rust src/components/Counter.rs | ||
use sweet::prelude::*; | ||
use sweet::prelude::set_target_text; | ||
|
||
pub struct Counter{ | ||
pub initial_value: usize | ||
} | ||
impl Component for Counter{ | ||
fn rsx(self) -> Rsx { | ||
let mut count = 0; | ||
|
||
let onclick = |e| { | ||
count += 1; | ||
set_target_text(e, format!("You clicked {} times", count)); | ||
} | ||
|
||
rsx!{ | ||
<button onclick>You clicked 0 times</button> | ||
} | ||
} | ||
} | ||
``` | ||
|
||
To sweeten the developer flow this time we'll allow sweet to manage our run command with hot reloading. | ||
```sh | ||
sweet run | ||
``` | ||
Nows a great time to check out the instant html reloading, lets move the text into a div. | ||
```rust | ||
rsx!{ | ||
<div>You clicked 0 times</div> | ||
<button>Increment</button> | ||
} | ||
``` | ||
|
||
Sweet as! We have hot reloading but now our counter is broken. Lets fix this with a signal. | ||
|
||
```rust | ||
fn rsx(self) -> Rsx { | ||
let (count,set_count) = signal(0) | ||
|
||
let onclick = |e| { | ||
set_count.update(|c| c += 1 ) | ||
} | ||
|
||
rsx!{ | ||
<div>You clicked {count} times</div> | ||
<button onclick>Increment</button> | ||
} | ||
} | ||
``` | ||
And viola, our counter is complete, at this point you may be thinking this looks a lot like leptos/solid, and thats because it is! At least its the core reactive system of leptos being used with preprocessed rsx instead of compile-time macros. | ||
|
||
|
||
<!-- ## Next steps | ||
- If you want to ensure your counter doesn't go haywire check out this [testing guide]. | ||
- Beautify your counter with scoped styles or the built-in component library --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
--- | ||
title: On Assert | ||
description: The dark side of assert | ||
draft: true | ||
sidebar: | ||
order: 99 | ||
--- | ||
|
||
First of all let me assert that `assert!` does have a place, but its absolutely not in tests. The TLDR is that it provides a simple way to collect error locations but strikes at rust's achilles heel, *compile times*, and the same information can be achieve lazily through runtime backtracing. | ||
|
||
## The Bench | ||
|
||
- [source code](https://github.com/mrchantey/sweet/blob/main/cli/src/bench/bench_assert.rs) | ||
- have a play `cargo install sweet-cli && sweet bench-assert --iterations 2000` | ||
|
||
|
||
Its common knowledge that even simple macros increase compile times, but did you ever wonder how much? The answer turns out to be a few milliseconds. The below benches were created by generating files with `n` lines of either `assert!` or a wrapper `expect` function. | ||
|
||
|
||
_I dont consider myself a benching wizard, if you see a way this approach could be improved please [file an issue](https://github.com/mrchantey/sweet/issues) or pr. I'm particularly curious about what happened at the 20,000 line mark._ | ||
|
||
### Implications: | ||
|
||
For some real world context, here's some 'back of a napkin' calculations i did by grepping a few rust repos i had laying around: | ||
| Repo | `assert!` Lines [^3] | `assert!` Compile Time | `expect` Compile Time | | ||
| ------------ | -------------------- | ---------------------- | --------------------- | | ||
| bevy | 7,000 | 30s | 0.3s | | ||
| wasm-bindgen | 3,000 | 15s | 0.15s | | ||
| rust | 50,000 | 250s | 2.5s | | ||
|
||
[^3]: A very coarse grep of `assert!` or `assert_` | ||
|
||
### Assert: `5ms` | ||
|
||
Creating a file with `n` number of lines with an `assert_eq!(n,n)`, calcualting how long it takes to compile the assert! macro. | ||
|
||
| Lines | Compilation [^1] | Time per Line [^2] | Notes | | ||
| ------ | ---------------- | ------------------ | ------------------------------------------- | | ||
| 10 | 0.21s | 21.00ms | | | ||
| 100 | 0.23s | 2.30ms | | | ||
| 1,000 | 1.54s | 1.54ms | | | ||
| 2,000 | 4.92s | 2.46ms | | | ||
| 3,000 | 11.61s | 3.87ms | | | ||
| 5,000 | 26.96s | 5.39ms | | | ||
| 10,000 | 55.00s | 5.50ms | | | ||
| 20,000 | 1.06s | 0.05ms | this is incorrect, it actually took 10 mins | | ||
|
||
|
||
### Expect: `0.05ms` | ||
|
||
Creating a file with `n` number of lines with an assert! wrapper function called `expect(n,n)`. This bench essentially calculates how long it takes to compile the calling of a regular rust function. | ||
|
||
| Lines | Compilation [^1] | Time per Line [^2] | | ||
| ------- | ---------------- | ------------------ | | ||
| 10 | 0.53s | 53.00ms | | ||
| 100 | 0.47s | 4.70ms | | ||
| 1,000 | 0.49s | 0.49ms | | ||
| 2,000 | 0.50s | 0.25ms | | ||
| 3,000 | 0.53s | 0.18ms | | ||
| 5,000 | 0.56s | 0.11ms | | ||
| 10,000 | 0.70s | 0.07ms | | ||
| 20,000 | 1.06s | 0.05ms | | ||
| 100,000 | 5.37s | 0.05ms | | ||
| 500,000 | 44.**00s** | 0.09ms | | ||
|
||
[^1]: Compile times are retrieved from the output of `cargo build`, `Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs` | ||
[^2]: Time per line is simply ` line count / compile time` | ||
|
||
## The Alternative - Matchers | ||
|
||
The alternative requires both the matcher and the runner to work in unison with three rules: | ||
|
||
1. The `expect()` function must panic exactly one frame beneath the caller and always outputs some prefix in the payload string, in sweet this is `"Sweet Error:"` | ||
2. If the runner encounters a regular panic, just use the panics location for pretty printing. | ||
3. If the runner encounters a panic with the prefix, create a backtracer and use the location exactly one frame up the callstack. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
--- | ||
title: Async | ||
description: Running async tests | ||
draft: true | ||
sidebar: | ||
order: 3 | ||
--- | ||
|
||
## `#[tokio::test]` (native) | ||
|
||
Sweet supports `#[tokio::test]` and any other macro that runs with the default test runner, and they will run in the same fashion. | ||
|
||
## `#[wasm_bindgen_test` (wasm) | ||
|
||
These tests are run in the `wasm_bindgen_test` runner and cannot be accessed by `sweet`. | ||
|
||
## `#[sweet::test]` (native,wasm) | ||
|
||
The sweet test macro works differently from tokio in that it employs a shared async runtime which results in faster startup times. For 99% of cases `#[sweet::test]` is the way to go, but if you do have two tests mutating the same static resources. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
--- | ||
title: Sweet | ||
description: Delightful Rust testing | ||
draft: true | ||
sidebar: | ||
label: Overview | ||
--- | ||
|
||
The sweet test runner is built from the ground up with web dev in mind, the `#[sweet::test]` macro will run any test, be it sync, async, native or wasm. | ||
|
||
It unifies native and wasm async tests, integrates with existing `#[test]` suites and outperforms other runners. | ||
|
||
## Performance | ||
|
||
- Sweet matchers compile [100x faster](./assert.md) than `assert!` macros. | ||
- `#[sweet::test]` outperforms `#[tokio::test]` through shared async runtimes. | ||
|
||
## Versatility | ||
|
||
There is a range of use-cases beyond the humble `#[test]` and that has led to the creation of several test crates: | ||
- `#[wasm_bindgen_test]` provides wasm support for both standard and async testing. | ||
- `#[tokio::test]` allows for isolated async tests. | ||
- `#[tokio_shared_rt::test]` adds the option of a shared tokio runtime, with resource sharing and faster startup times. | ||
|
||
Sweet supports all of these under a single `#[sweet::test]` macro, and it also collects and runs `#[test]`, `#[tokio::test]`, etc. [^1] | ||
|
||
## The 1 Second Rule | ||
|
||
A correctly formatted test output should give the developer an intuition for what happend in less than a second, with verbosity flags for when its needed. Here is how sweet's approach differs from the default runner: | ||
|
||
1. **Clarity:** The most important part of an output is *the result*, so it goes before the test name. | ||
2. **Brevity:** File paths are shorter than fully qualified module names, increasing readability and reducing likelihood of line breaks. | ||
4. **Linkability:** The use of file paths turns the test output into a sort of menu, viewing a specific file is a control+click away. | ||
3. **Organization:** Files are the goldilocks level of output between individual test cases and entire binaries, so thats the default suite organization and unit of output. | ||
|
||
```rust | ||
// default output | ||
test my_crate::my_module::test::my_test ... ok | ||
// sweet output | ||
PASS src/my_module.rs | ||
``` | ||
|
||
## Inspiration | ||
|
||
If the tagline or runner output format look familar that because they are copied verbatum from my favourite test runner, [Jest](https://jestjs.io/). | ||
The handling of async wasm panics was made possible by the wizards over at `wasm_bindgen_test`. | ||
|
||
[^1]: An exception to this is `#[wasm_bindgen_test]` because it uses a custom runner. |
Oops, something went wrong.