Skip to content

Commit e9c4b49

Browse files
authored
feat: viz integration (leptos-rs#506)
1 parent f7d0eea commit e9c4b49

22 files changed

+1752
-5
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
# integrations
1212
"integrations/actix",
1313
"integrations/axum",
14+
"integrations/viz",
1415
"integrations/utils",
1516

1617
# libraries

Makefile.toml

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies = [
4040
{ name = "check", path = "examples/tailwind" },
4141
{ name = "check", path = "examples/todo_app_sqlite" },
4242
{ name = "check", path = "examples/todo_app_sqlite_axum" },
43+
{ name = "check", path = "examples/todo_app_sqlite_viz" },
4344
{ name = "check", path = "examples/todomvc" },
4445
]
4546

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
[package]
2+
name = "todo_app_sqlite_viz"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib", "rlib"]
8+
9+
[dependencies]
10+
anyhow = "1.0.66"
11+
console_log = "0.2.0"
12+
console_error_panic_hook = "0.1.7"
13+
futures = "0.3.25"
14+
cfg-if = "1.0.0"
15+
leptos = { path = "../../leptos", default-features = false, features = [
16+
"serde",
17+
] }
18+
leptos_viz = { path = "../../integrations/viz", default-features = false, optional = true }
19+
leptos_meta = { path = "../../meta", default-features = false }
20+
leptos_router = { path = "../../router", default-features = false }
21+
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
22+
log = "0.4.17"
23+
simple_logger = "4.0.0"
24+
serde = { version = "1.0.148", features = ["derive"] }
25+
serde_json = "1.0.89"
26+
gloo-net = { version = "0.2.5", features = ["http"] }
27+
reqwest = { version = "0.11.13", features = ["json"] }
28+
viz = { version = "0.4.8", features = ["serve"], optional = true }
29+
tokio = { version = "1.25.0", features = ["full"], optional = true }
30+
http = { version = "0.2.8" }
31+
sqlx = { version = "0.6.2", features = [
32+
"runtime-tokio-rustls",
33+
"sqlite",
34+
], optional = true }
35+
thiserror = "1.0.38"
36+
tracing = "0.1.37"
37+
wasm-bindgen = "0.2"
38+
39+
[features]
40+
default = ["csr"]
41+
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
42+
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
43+
ssr = [
44+
"dep:viz",
45+
"dep:tokio",
46+
"dep:sqlx",
47+
"leptos/ssr",
48+
"leptos_meta/ssr",
49+
"leptos_router/ssr",
50+
"dep:leptos_viz"
51+
]
52+
53+
[package.metadata.cargo-all-features]
54+
denylist = ["viz", "tokio", "sqlx", "leptos_viz"]
55+
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
56+
57+
[package.metadata.leptos]
58+
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
59+
output-name = "todo_app_sqlite_viz"
60+
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
61+
site-root = "target/site"
62+
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
63+
# Defaults to pkg
64+
site-pkg-dir = "pkg"
65+
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
66+
style-file = "./style.css"
67+
# [Optional] Files in the asset-dir will be copied to the site-root directory
68+
assets-dir = "public"
69+
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
70+
site-addr = "127.0.0.1:3000"
71+
# The port to use for automatic reload monitoring
72+
reload-port = 3001
73+
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
74+
end2end-cmd = "npx playwright test"
75+
# The browserlist query used for optimizing the CSS.
76+
browserquery = "defaults"
77+
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
78+
watch = false
79+
# The environment Leptos will run in, usually either "DEV" or "PROD"
80+
env = "DEV"
81+
# The features to use when compiling the bin target
82+
#
83+
# Optional. Can be over-ridden with the command line parameter --bin-features
84+
bin-features = ["ssr"]
85+
86+
# If the --no-default-features flag should be used when compiling the bin target
87+
#
88+
# Optional. Defaults to false.
89+
bin-default-features = false
90+
91+
# The features to use when compiling the lib target
92+
#
93+
# Optional. Can be over-ridden with the command line parameter --lib-features
94+
lib-features = ["hydrate"]
95+
96+
# If the --no-default-features flag should be used when compiling the lib target
97+
#
98+
# Optional. Defaults to false.
99+
lib-default-features = false

examples/todo_app_sqlite_viz/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Greg Johnston
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[tasks.build]
2+
command = "cargo"
3+
args = ["+nightly", "build-all-features"]
4+
install_crate = "cargo-all-features"
5+
6+
[tasks.check]
7+
command = "cargo"
8+
args = ["+nightly", "check-all-features"]
9+
install_crate = "cargo-all-features"
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Leptos Todo App Sqlite with Viz
2+
3+
This example creates a basic todo app with a Viz backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
4+
5+
## Client Side Rendering
6+
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
7+
8+
## Server Side Rendering with cargo-leptos
9+
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
10+
11+
1. Install cargo-leptos
12+
```bash
13+
cargo install --locked cargo-leptos
14+
```
15+
2. Build the site in watch mode, recompiling on file changes
16+
```bash
17+
cargo leptos watch
18+
```
19+
20+
Open browser on [http://localhost:3000/](http://localhost:3000/)
21+
22+
3. When ready to deploy, run
23+
```bash
24+
cargo leptos build --release
25+
```
26+
27+
## Server Side Rendering without cargo-leptos
28+
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
29+
30+
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
31+
1. Install wasm-pack
32+
```bash
33+
cargo install wasm-pack
34+
```
35+
2. Build the Webassembly used to hydrate the HTML from the server
36+
```bash
37+
wasm-pack build --target=web --debug --no-default-features --features=hydrate
38+
```
39+
3. Run the server to serve the Webassembly, JS, and HTML
40+
```bash
41+
cargo run --no-default-features --features=ssr
42+
```

examples/todo_app_sqlite_viz/Todos.db

16 KB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
CREATE TABLE IF NOT EXISTS todos
3+
(
4+
id INTEGER NOT NULL PRIMARY KEY,
5+
title VARCHAR,
6+
completed BOOLEAN
7+
);
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use crate::errors::TodoAppError;
2+
use cfg_if::cfg_if;
3+
use leptos::{Errors, *};
4+
#[cfg(feature = "ssr")]
5+
use leptos_viz::ResponseOptions;
6+
7+
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
8+
// here than just displaying them
9+
#[component]
10+
pub fn ErrorTemplate(
11+
cx: Scope,
12+
#[prop(optional)] outside_errors: Option<Errors>,
13+
#[prop(optional)] errors: Option<RwSignal<Errors>>,
14+
) -> impl IntoView {
15+
let errors = match outside_errors {
16+
Some(e) => create_rw_signal(cx, e),
17+
None => match errors {
18+
Some(e) => e,
19+
None => panic!("No Errors found and we expected errors!"),
20+
},
21+
};
22+
23+
// Get Errors from Signal
24+
// Downcast lets us take a type that implements `std::error::Error`
25+
let errors: Vec<TodoAppError> = errors
26+
.get()
27+
.into_iter()
28+
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
29+
.collect();
30+
31+
// Only the response code for the first error is actually sent from the server
32+
// this may be customized by the specific application
33+
cfg_if! {
34+
if #[cfg(feature="ssr")]{
35+
let response = use_context::<ResponseOptions>(cx);
36+
if let Some(response) = response{
37+
response.set_status(errors[0].status_code());
38+
}
39+
}
40+
}
41+
42+
view! {cx,
43+
<h1>"Errors"</h1>
44+
<For
45+
// a function that returns the items we're iterating over; a signal is fine
46+
each= move || {errors.clone().into_iter().enumerate()}
47+
// a unique key for each item as a reference
48+
key=|(index, _error)| *index
49+
// renders each item to a view
50+
view= move |cx, error| {
51+
let error_string = error.1.to_string();
52+
let error_code= error.1.status_code();
53+
view! {
54+
cx,
55+
<h2>{error_code.to_string()}</h2>
56+
<p>"Error: " {error_string}</p>
57+
}
58+
}
59+
/>
60+
}
61+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use http::status::StatusCode;
2+
use thiserror::Error;
3+
4+
#[derive(Debug, Clone, Error)]
5+
pub enum TodoAppError {
6+
#[error("Not Found")]
7+
NotFound,
8+
#[error("Internal Server Error")]
9+
InternalServerError,
10+
}
11+
12+
impl TodoAppError {
13+
pub fn status_code(&self) -> StatusCode {
14+
match self {
15+
TodoAppError::NotFound => StatusCode::NOT_FOUND,
16+
TodoAppError::InternalServerError => {
17+
StatusCode::INTERNAL_SERVER_ERROR
18+
}
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use cfg_if::cfg_if;
2+
3+
cfg_if! {
4+
if #[cfg(feature = "ssr")] {
5+
use crate::{
6+
error_template::{ErrorTemplate, ErrorTemplateProps},
7+
errors::TodoAppError,
8+
};
9+
use http::Uri;
10+
use leptos::{view, Errors, LeptosOptions};
11+
use std::sync::Arc;
12+
use viz::{
13+
handlers::serve, header::HeaderMap, types::RouteInfo, Body, Error, Handler,
14+
Request, RequestExt, Response, ResponseExt, Result,
15+
};
16+
17+
pub async fn file_and_error_handler(req: Request<Body>) -> Result<Response> {
18+
let uri = req.uri().clone();
19+
let headers = req.headers().clone();
20+
let route_info = req.route_info().clone();
21+
let options = &*req.state::<Arc<LeptosOptions>>().ok_or(
22+
Error::Responder(Response::text("missing state type LeptosOptions")),
23+
)?;
24+
let root = &options.site_root;
25+
let resp = get_static_file(uri, &root, headers, route_info).await?;
26+
let status = resp.status();
27+
28+
if status.is_success() || status.is_redirection() {
29+
Ok(resp)
30+
} else {
31+
let mut errors = Errors::default();
32+
errors.insert_with_default_key(TodoAppError::NotFound);
33+
let handler = leptos_viz::render_app_to_stream(
34+
options.to_owned(),
35+
move |cx| view! {cx, <ErrorTemplate outside_errors=errors.clone()/>},
36+
);
37+
handler(req).await
38+
}
39+
}
40+
41+
async fn get_static_file(
42+
uri: Uri,
43+
root: &str,
44+
headers: HeaderMap,
45+
route_info: Arc<RouteInfo>,
46+
) -> Result<Response> {
47+
let mut req = Request::builder()
48+
.uri(uri.clone())
49+
.extension(route_info)
50+
.body(Body::empty())
51+
.unwrap();
52+
*req.headers_mut() = headers;
53+
// This path is relative to the cargo root
54+
serve::Dir::new(root).call(req).await
55+
}
56+
57+
}
58+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use cfg_if::cfg_if;
2+
use leptos::*;
3+
pub mod error_template;
4+
pub mod errors;
5+
pub mod fallback;
6+
pub mod todo;
7+
8+
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
9+
cfg_if! {
10+
if #[cfg(feature = "hydrate")] {
11+
use wasm_bindgen::prelude::wasm_bindgen;
12+
use crate::todo::*;
13+
14+
#[wasm_bindgen]
15+
pub fn hydrate() {
16+
console_error_panic_hook::set_once();
17+
_ = console_log::init_with_level(log::Level::Debug);
18+
console_error_panic_hook::set_once();
19+
20+
leptos::mount_to_body(|cx| {
21+
view! { cx, <TodoApp/> }
22+
});
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)