Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mrchantey committed Jan 13, 2025
1 parent 5d00593 commit b7afb17
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 2 deletions.
60 changes: 60 additions & 0 deletions crates/sweet_site/src/collections/docs/how-it-works.md
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
18 changes: 18 additions & 0 deletions crates/sweet_site/src/collections/docs/index.md
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)
5 changes: 5 additions & 0 deletions crates/sweet_site/src/collections/docs/markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Using Markdown

---

112 changes: 112 additions & 0 deletions crates/sweet_site/src/collections/docs/quickstart.md
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 -->
75 changes: 75 additions & 0 deletions crates/sweet_site/src/collections/docs/testing/assert.md
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.
19 changes: 19 additions & 0 deletions crates/sweet_site/src/collections/docs/testing/aysnc.md
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.
48 changes: 48 additions & 0 deletions crates/sweet_site/src/collections/docs/testing/index.md
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.
Loading

0 comments on commit b7afb17

Please sign in to comment.