Compare commits

..

98 Commits

Author SHA1 Message Date
Greg Johnston
55ef106c50 initial work on Actix middleware support 2024-01-19 16:35:51 -05:00
Greg Johnston
26d9d75cf2 cargo fmt 2024-01-19 15:56:57 -05:00
Greg Johnston
46e7abf9ba allow custom req/res/client types 2024-01-19 15:48:14 -05:00
Greg Johnston
1b1e02729e clean up examples 2024-01-19 15:17:17 -05:00
Greg Johnston
fdd576535a clean up examples 2024-01-19 15:14:39 -05:00
Greg Johnston
2a9e502893 fix rkyv deserialization 2024-01-19 15:03:21 -05:00
Greg Johnston
a519859a66 Revert "use &[u8] instead of Bytes for requests"
This reverts commit e179db1d42.
2024-01-19 14:37:03 -05:00
Greg Johnston
25120c0e9f fix streaming requests and clarify in docs 2024-01-19 14:17:26 -05:00
Greg Johnston
94cb4c0ec3 remove pavex work (now in pavex branch) 2024-01-19 14:17:26 -05:00
Greg Johnston
f9cd8539e4 add missing PartialEq/Eq implementations on ServerFnError (closes #2198) 2024-01-19 14:17:26 -05:00
Greg Johnston
14072457d0 clean up docs (closes #2197) 2024-01-19 14:17:26 -05:00
Greg Johnston
e179db1d42 use &[u8] instead of Bytes for requests 2024-01-19 14:17:26 -05:00
Greg Johnston
2fa60103b4 share inventory collect across types 2024-01-19 14:17:26 -05:00
Greg Johnston
a3a15f244d expose all fields of ServerFnTraitObj via methods 2024-01-19 14:17:26 -05:00
Greg Johnston
0df5dfeaf8 weak dependency on Cargo.toml 2024-01-19 14:17:26 -05:00
Greg Johnston
3f22906053 fix warning 2024-01-19 14:17:26 -05:00
Greg Johnston
33ad30515d serde-lite support should be enabled directly on server_fn 2024-01-19 14:17:26 -05:00
Greg Johnston
c5bab09423 partial support for streaming requests (doesn't actually work in the browser) 2024-01-19 14:17:26 -05:00
Greg Johnston
320179bc04 remove misleading warning 2024-01-19 14:17:26 -05:00
Greg Johnston
5065bed594 example of middleware that can run before and/or after server fn 2024-01-19 14:17:26 -05:00
Greg Johnston
22b4537f27 fix version numbers 2024-01-19 14:17:26 -05:00
Greg Johnston
8d23d5136a add package metadata 2024-01-19 14:17:25 -05:00
Greg Johnston
c7fac64054 fix merge error 2024-01-19 14:17:25 -05:00
Greg Johnston
047235e7c1 clippy 2024-01-19 14:17:25 -05:00
Greg Johnston
7a086ad159 update version number 2024-01-19 14:17:25 -05:00
Greg Johnston
bb923b3f9b erroneous hyphen 2024-01-19 14:16:59 -05:00
Greg Johnston
6a8c26a820 streaming example with filesystem watcher 2024-01-19 14:16:59 -05:00
Greg Johnston
21f8085851 add streaming/file watcher example 2024-01-19 14:16:59 -05:00
Greg Johnston
9a5a102ce3 add middleware to kitchen-sink example 2024-01-19 14:16:59 -05:00
Greg Johnston
4d602c21f8 example with custom errors 2024-01-19 14:16:59 -05:00
Greg Johnston
7d114c7414 file upload example 2024-01-19 14:16:58 -05:00
Greg Johnston
1f017a2ade hm custom encodings have orphan rule issues 2024-01-19 14:16:58 -05:00
Greg Johnston
35e8e74dcf get rkyv working and work on custom encoding example 2024-01-19 14:16:58 -05:00
Markus Kohlhase
4366d786ac Update login example (CSR only) (#2155) 2024-01-19 14:16:58 -05:00
Ari Seyhun
1777a4057a fix!: remove clone in Cow<'static, str> IntoView impl (#1946) 2024-01-19 14:16:58 -05:00
Greg Johnston
0571ebbc36 working on example 2024-01-19 14:16:58 -05:00
Greg Johnston
06c478b7cb feature-gate the form redirect stuff, and clear old errors from query 2024-01-19 14:16:58 -05:00
Greg Johnston
90ba3529e9 working on Axum version 2024-01-19 14:16:58 -05:00
Greg Johnston
13a2691806 working on server fn example 2024-01-19 14:16:58 -05:00
Greg Johnston
1ad7ee8a03 generalize error redirect behavior across integrations 2024-01-19 14:16:58 -05:00
Greg Johnston
88fee243a8 support setting server URL on either platform 2024-01-19 14:16:58 -05:00
Greg Johnston
5e08253521 get both client and server side working 2024-01-19 14:16:58 -05:00
Greg Johnston
cc6f65cd83 initial version of server action error handling without JS 2024-01-19 14:16:58 -05:00
Greg Johnston
9488114801 docs 2024-01-19 14:16:58 -05:00
Greg Johnston
b0cdeab906 remove old code 2024-01-19 14:16:58 -05:00
Greg Johnston
def4be80b2 docs 2024-01-19 14:16:58 -05:00
Greg Johnston
15b04a8a85 more docs 2024-01-19 14:16:58 -05:00
Greg Johnston
0a9cdba22e getting started on docs 2024-01-19 14:16:58 -05:00
Greg Johnston
1d1de4ac38 remove cfg-if from all examples 2024-01-19 14:16:58 -05:00
Greg Johnston
31b2b9e94c remove explicit handle_server_fns in most cases because it's now included in .leptos_routes() 2024-01-19 14:16:58 -05:00
Greg Johnston
8f07818687 nicer formatting, remove cfg-if 2024-01-19 14:16:58 -05:00
Greg Johnston
a5cbfa0aad remove viz integration (see #2177) 2024-01-19 14:16:58 -05:00
Greg Johnston
6c8e704fb3 smh 2024-01-19 14:16:58 -05:00
Greg Johnston
81fb5160e5 missing makefiles 2024-01-19 14:16:58 -05:00
Greg Johnston
2af0d3d781 update session_auth_axum 2024-01-19 14:16:58 -05:00
Greg Johnston
7f532cda70 update todo_app_sqlite_csrs 2024-01-19 14:16:58 -05:00
Greg Johnston
c7941f7639 clippy 2024-01-19 14:16:58 -05:00
Greg Johnston
61148026d1 allow type paths for input/output, and properly namespace built-in encodings 2024-01-19 14:16:58 -05:00
Greg Johnston
738eeefe73 chore: clear warnings 2024-01-19 14:16:18 -05:00
Greg Johnston
be084a5d1d remove list of magic identifiers, use rust-analyzer to help with imports instead 2024-01-19 14:16:18 -05:00
Greg Johnston
f5c007df7b use server fns directly in ActionForm and MultiActionForm 2024-01-19 14:16:18 -05:00
Rakshith Ravi
a1bd84f3dc feat: add serde-lite codec for server functions (#2168) 2024-01-19 14:16:18 -05:00
Rakshith Ravi
f6ce82c9d1 Fixed tests for server_fn (#2167)
* Fixed server_fn tests

* Changed type_name to TypeId

* Fixed handling of leading slashes for server_fn endpoint
2024-01-19 14:16:18 -05:00
Greg Johnston
853c080707 add missing server fn registration 2024-01-19 14:16:18 -05:00
Greg Johnston
f6b95e40f4 make sure endpoint names begin with a / 2024-01-19 14:16:18 -05:00
Greg Johnston
db1497b9c2 set version, input, etc. correctly 2024-01-19 14:16:18 -05:00
Greg Johnston
f53ac1a4ae remove unused var 2024-01-19 14:16:18 -05:00
Greg Johnston
5e6f4403ca set up redirects in Actix 2024-01-19 14:16:18 -05:00
Greg Johnston
4e3f1c834c handle client-side and server-side redirects correctly (in Axum) 2024-01-19 14:16:18 -05:00
Greg Johnston
566df034ff actually use server functions in ActionForm 2024-01-19 14:16:17 -05:00
Greg Johnston
fd97e2e027 Restore the previous full functionality of Form 2024-01-19 14:16:17 -05:00
Greg Johnston
c8fbee18c8 finished Actix support? 2024-01-19 14:16:17 -05:00
Greg Johnston
e1a9856ca9 more Actix work 2024-01-19 14:16:17 -05:00
Greg Johnston
60efaefff4 start Actix work 2024-01-19 14:16:17 -05:00
Greg Johnston
db4158f5c3 clear up warnings 2024-01-19 14:16:17 -05:00
Greg Johnston
af62d2e900 automatically include server function handler in .leptos_router() 2024-01-19 14:16:17 -05:00
Greg Johnston
c3e3ce7878 changes to get todo_app_sqlite_axum example working 2024-01-19 14:16:17 -05:00
Greg Johnston
dec17fc65b fix server actions and server multi actions 2024-01-19 14:16:03 -05:00
Greg Johnston
2dbc5899f3 cargo fmt 2024-01-19 14:16:03 -05:00
Greg Johnston
dd368a845c @ealmloff changes to reexport actix/axum 2024-01-19 14:16:03 -05:00
Greg Johnston
9c258219dd fix Actix implementation with middleware 2024-01-19 14:16:03 -05:00
Greg Johnston
6a1685936b fix rkyv 2024-01-19 14:16:03 -05:00
Greg Johnston
7d45e6bb13 clean up my mistake 2024-01-19 14:16:03 -05:00
Greg Johnston
8fae76828e FromStr-based lightweight ServerFnError deserialization 2024-01-19 14:16:03 -05:00
Greg Johnston
d5b9e84f36 properly gate inventory 2024-01-19 14:16:03 -05:00
benwis
197edebd51 Made some progress, started work on pavex integration as well 2024-01-19 14:16:03 -05:00
benwis
2a5c855595 It starts to compile! 2024-01-19 14:16:03 -05:00
benwis
c9627bfeb4 Setup folder structure as before. Got a cyclical dependency though 2024-01-19 14:16:03 -05:00
benwis
c7422cd96e First commit, checkpoint for cyclical dependency error 2024-01-19 14:15:51 -05:00
Daniel Santana
cadd217078 Update integration with support for axum 0.7 (#2082)
* chore: update to axum 0.7

Removed http, since it's included in axum, and replaced hyper by http-body-util, which is a smaller.

* chore: update samples to work with nre axum

Missing sessions_axum_auth, pending PR merge.

* chore: all dependencies update to axum 0.7

* chore: cargo fmt

* chore: fix doctests

* chore: Fix example that in reality doesn't use axum.

Fixed anyway.

* chore: more examples support for axum 0.7

* Small tweak
2024-01-19 14:13:55 -05:00
Greg Johnston
0c4cf5471d v0.5.7 2024-01-19 13:03:44 -05:00
Greg Johnston
dd0c349554 examples: update axum-session because old version was yanked (#2205) 2024-01-19 12:54:08 -05:00
Greg Johnston
dd5a0ae094 Merge pull request #2203 from leptos-rs/2201
fix: routing regressions caused by `trailing_slash` support
2024-01-19 12:14:20 -05:00
Greg Johnston
5cacb57283 chore: new clippy warnings 2024-01-19 11:14:36 -05:00
Greg Johnston
b356d3cd28 ci: add regression test for #2190 2024-01-19 10:29:30 -05:00
Greg Johnston
ae1de88916 Revert "Better handling for trailing slashes. (#2154) (#2172)"
This reverts commit 1eaf886481.
2024-01-19 10:27:38 -05:00
Greg Johnston
67dd188358 ci: add regression test for matching main page correctly in router example 2024-01-19 10:26:58 -05:00
Joseph Cruz
1d4772251a fix: ci stopped detecting leptos or example changes (#2194) 2024-01-17 18:58:21 -05:00
199 changed files with 8238 additions and 7014 deletions

View File

@@ -22,7 +22,6 @@ jobs:
[
integrations/actix,
integrations/axum,
integrations/viz,
integrations/utils,
leptos,
leptos_config,

View File

@@ -24,7 +24,7 @@ jobs:
uses: tj-actions/changed-files@v41
with:
files: |
examples
examples/**
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml

View File

@@ -22,18 +22,18 @@ jobs:
uses: tj-actions/changed-files@v41
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
integrations/**
leptos/**
leptos_config/**
leptos_dom/**
leptos_hot_reload/**
leptos_macro/**
leptos_reactive/**
leptos_server/**
meta/**
router/**
server_fn/**
server_fn_macro/**
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'

View File

@@ -16,7 +16,6 @@ members = [
# integrations
"integrations/actix",
"integrations/axum",
"integrations/viz",
"integrations/utils",
# libraries
@@ -26,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.6"
version = "0.6.0-beta"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.6" }
leptos_dom = { path = "./leptos_dom", version = "0.5.6" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.6" }
leptos_macro = { path = "./leptos_macro", version = "0.5.6" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.6" }
leptos_server = { path = "./leptos_server", version = "0.5.6" }
server_fn = { path = "./server_fn", version = "0.5.6" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.6" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.6" }
leptos_config = { path = "./leptos_config", version = "0.5.6" }
leptos_router = { path = "./router", version = "0.5.6" }
leptos_meta = { path = "./meta", version = "0.5.6" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.6" }
leptos = { path = "./leptos", version = "0.6.0-beta" }
leptos_dom = { path = "./leptos_dom", version = "0.6.0-beta" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-beta" }
leptos_macro = { path = "./leptos_macro", version = "0.6.0-beta" }
leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-beta" }
leptos_server = { path = "./leptos_server", version = "0.6.0-beta" }
server_fn = { path = "./server_fn", version = "0.6.0-beta" }
server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
leptos_config = { path = "./leptos_config", version = "0.6.0-beta" }
leptos_router = { path = "./router", version = "0.6.0-beta" }
leptos_meta = { path = "./meta", version = "0.6.0-beta" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-beta" }
[profile.release]
codegen-units = 1

View File

@@ -21,6 +21,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"login_with_token_csr_only",
"parent_child",
"router",
"server_fns_axum",
"session_auth_axum",
"slots",
"ssr_modes",
@@ -32,7 +33,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]

View File

@@ -0,0 +1,90 @@
[package]
name = "action-form-error-handling"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
cfg-if = "1"
http = { version = "0.2", optional = true }
leptos = { path = "../../leptos" }
leptos_meta = { path = "../../meta" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos_start"
# 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.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [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
style-file = "style/main.scss"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View File

@@ -5,4 +5,4 @@ extend = [
[env]
CLIENT_PROCESS_NAME = "todo_app_sqlite_viz"
CLIENT_PROCESS_NAME = "action_form_error_handling"

View File

@@ -0,0 +1,68 @@
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
# Leptos Starter Template
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
## Creating your template repo
If you don't have `cargo-leptos` installed you can install it with
`cargo install cargo-leptos`
Then run
`cargo leptos new --git leptos-rs/start`
to generate a new project template (you will be prompted to enter a project name).
`cd {projectname}`
to go to your newly created project.
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
## Running your project
`cargo leptos watch`
By default, you can access your local project at `http://localhost:3000`
## Installing Additional Tools
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
## Executing a Server on a Remote Machine Without the Toolchain
After running a `cargo leptos build --release` the minimum files needed are:
1. The server binary located in `target/server/release`
2. The `site` directory and all files within located in `target/site`
Copy these files to your remote server. The directory structure should be:
```text
leptos_start
site/
```
Set the following environment variables (updating for your project as needed):
```sh
export LEPTOS_OUTPUT_NAME="leptos_start"
export LEPTOS_SITE_ROOT="site"
export LEPTOS_SITE_PKG_DIR="pkg"
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
export LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.
## Notes about CSR and Trunk:
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
`trunk serve --open --features csr`
This may be useful for integrating external tools which require a static site, e.g. `tauri`.

View File

@@ -0,0 +1,97 @@
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router>
<main id="app">
<Routes>
<Route path="" view=HomePage/>
<Route path="/*any" view=NotFound/>
</Routes>
</main>
</Router>
}
}
#[server]
async fn do_something(
should_error: Option<String>,
) -> Result<String, ServerFnError> {
if should_error.is_none() {
Ok(String::from("Successful submit"))
} else {
Err(ServerFnError::ServerError(String::from(
"You got an error!",
)))
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let do_something_action = Action::<DoSomething, _>::server();
let value = Signal::derive(move || {
do_something_action
.value()
.get()
.unwrap_or_else(|| Ok(String::new()))
});
Effect::new_isomorphic(move |_| {
logging::log!("Got value = {:?}", value.get());
});
view! {
<h1>"Test the action form!"</h1>
<ErrorBoundary fallback=move |error| format!("{:#?}", error
.get()
.into_iter()
.next()
.unwrap()
.1.into_inner()
.to_string())
>
{value}
<ActionForm action=do_something_action class="form">
<label>Should error: <input type="checkbox" name="should_error"/></label>
<button type="submit">Submit</button>
</ActionForm>
</ErrorBoundary>
}
}
/// 404 - Not Found
#[component]
fn NotFound() -> impl IntoView {
// set an HTTP status code 404
// this is feature gated because it can only be done during
// initial server-side rendering
// if you navigate to the 404 page subsequently, the status
// code will not be set because there is not a new HTTP request
// to the server
#[cfg(feature = "ssr")]
{
// this can be done inline because it's synchronous
// if it were async, we'd use a server function
let resp = expect_context::<leptos_actix::ResponseOptions>();
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
}
view! {
<h1>"Not Found"</h1>
}
}

View File

@@ -0,0 +1,18 @@
pub mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}
}

View File

@@ -0,0 +1,53 @@
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use action_form_error_handling::app::*;
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
println!("listening on http://{}", &addr);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.app_data(web::Data::new(leptos_options.to_owned()))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
#[cfg(not(any(feature = "ssr", feature = "csr")))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
// see optional feature `csr` instead
}
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
pub fn main() {
// a client-side main function is required for using `trunk serve`
// prefer using `cargo leptos serve` instead
// to run: `trunk serve --open --features csr`
use action_form_error_handling::app::*;
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -0,0 +1,15 @@
body {
font-family: sans-serif;
text-align: center;
}
#app {
text-align: center;
}
.form {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}

View File

@@ -17,7 +17,6 @@ broadcaster = "1"
console_log = "1"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }

View File

@@ -1,34 +1,35 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
use tracing::instrument;
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::sync::atomic::{AtomicI32, Ordering};
use broadcaster::BroadcastChannel;
use once_cell::sync::OnceCell;
#[cfg(feature = "ssr")]
pub mod ssr_imports {
pub use broadcaster::BroadcastChannel;
pub use once_cell::sync::OnceCell;
pub use std::sync::atomic::{AtomicI32, Ordering};
static COUNT: AtomicI32 = AtomicI32::new(0);
pub static COUNT: AtomicI32 = AtomicI32::new(0);
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
static LOG_INIT: OnceCell<()> = OnceCell::new();
fn init_logging() {
LOG_INIT.get_or_init(|| {
simple_logger::SimpleLogger::new().env().init().unwrap();
});
}
static LOG_INIT: OnceCell<()> = OnceCell::new();
pub fn init_logging() {
LOG_INIT.get_or_init(|| {
simple_logger::SimpleLogger::new().env().init().unwrap();
});
}
}
#[server]
#[cfg_attr(feature = "ssr", instrument)]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
use ssr_imports::*;
Ok(COUNT.load(Ordering::Relaxed))
}
@@ -38,6 +39,8 @@ pub async fn adjust_server_count(
delta: i32,
msg: String,
) -> Result<i32, ServerFnError> {
use ssr_imports::*;
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&new).await;
@@ -48,6 +51,8 @@ pub async fn adjust_server_count(
#[server]
#[cfg_attr(feature = "ssr", instrument)]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
use ssr_imports::*;
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
Ok(0)
@@ -55,7 +60,7 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
#[component]
pub fn Counters() -> impl IntoView {
#[cfg(feature = "ssr")]
init_logging();
ssr_imports::init_logging();
provide_meta_context();
view! {

View File

@@ -1,21 +1,13 @@
use cfg_if::cfg_if;
pub mod counters;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::counters::*;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::counters::*;
use leptos::*;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <Counters/> }
});
}
}
mount_to_body(Counters);
}

View File

@@ -1,72 +1,54 @@
use cfg_if::cfg_if;
mod counters;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use leptos::*;
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use crate::counters::*;
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use futures::StreamExt;
#[get("/api/events")]
async fn counter_events() -> impl Responder {
use crate::counters::ssr_imports::*;
use futures::StreamExt;
let stream =
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetServerCount::register();
// _ = AdjustServerCount::register();
// _ = ClearServerCount::register();
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let routes = generate_route_list(|| view! { <Counters/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
}
// client-only main for Trunk
else {
pub fn main() {
// isomorphic counters cannot work in a Client-Side-Rendered only
// app as a server is required to maintain state
}
}
let stream = futures::stream::once(async {
crate::counters::get_server_count().await.unwrap_or(0)
})
.chain(COUNT_CHANNEL.clone())
.map(|value| {
Ok(web::Bytes::from(format!(
"event: message\ndata: {value}\n\n"
))) as Result<web::Bytes>
});
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let routes = generate_route_list(Counters);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(counter_events)
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
Counters,
)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}

View File

@@ -7,22 +7,21 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4.17"
log = "0.4"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
thiserror = "1.0.38"
simple_logger = "4.0"
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0" }
thiserror = "1.0"
wasm-bindgen = "0.2"
[features]

View File

@@ -1,5 +1,4 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::{logging::log, Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -30,12 +29,13 @@ pub fn ErrorTemplate(
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}}
}
view! {
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>

View File

@@ -1,43 +1,48 @@
use cfg_if::cfg_if;
use crate::landing::App;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions, view};
use crate::landing::App;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view!{ <App/> }
);
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! { <App/> },
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}}
}

View File

@@ -1,24 +1,21 @@
use cfg_if::cfg_if;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
pub mod landing;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::landing::*;
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
use crate::landing::*;
use leptos::*;
leptos::mount_to_body(|| {
view! { <App/> }
});
}
}
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|| {
view! { <App/> }
});
}

View File

@@ -1,41 +1,39 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{State, Path},
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
routing::get,
Router,
};
use errors_axum::*;
use leptos::{logging::log, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
}}
pub use errors_axum::{fallback::*, landing::App};
pub use leptos::{logging::log, *};
pub use leptos_axum::{generate_route_list, LeptosRoutes};
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
State(options): State<LeptosOptions>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
options.clone(),
move || {
provide_context(id.clone());
},
App,
);
handler(req).await.into_response()
// This custom handler lets us provide Axum State via context
pub async fn custom_handler(
Path(id): Path<String>,
State(options): State<LeptosOptions>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
options.clone(),
move || {
provide_context(id.clone());
},
App,
);
handler(req).await.into_response()
}
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use ssr_imports::*;
simple_logger::init_with_level(log::Level::Debug)
.expect("couldn't initialize logging");
@@ -52,7 +50,6 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
@@ -61,8 +58,8 @@ async fn main() {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
@@ -71,5 +68,5 @@ async fn main() {
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// The server is needed to demonstrate the error statuses.
// The server is needed to demonstrate the error statuses.
}

View File

@@ -15,7 +15,6 @@ actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }

View File

@@ -1,4 +1,3 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -33,16 +32,10 @@ pub fn App() -> impl IntoView {
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
}
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -1,56 +1,56 @@
use cfg_if::cfg_if;
use leptos::*;
// server-only stuff
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use actix_files::Files;
pub use actix_web::*;
pub use hackernews::App;
pub use leptos_actix::{generate_route_list, LeptosRoutes};
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use hackernews::{App};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[get("/favicon.ico")]
async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(css)
.service(favicon)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
} else {
fn main() {
use hackernews::{App};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App)
}
#[get("/style.css")]
pub async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[get("/favicon.ico")]
pub async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
}
}
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use leptos::get_configuration;
use ssr_imports::*;
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.service(css)
.service(favicon)
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
// CSR-only setup
#[cfg(not(feature = "ssr"))]
fn main() {
use hackernews::App;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App)
}

View File

@@ -11,24 +11,23 @@ codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.11", optional = true }
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"

View File

@@ -1,44 +1,48 @@
use cfg_if::cfg_if;
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::LeptosOptions;
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(None)
});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}

View File

@@ -1,63 +1,68 @@
use cfg_if::cfg_if;
use axum::{
body::Body,
http::{Request, Response, StatusCode, Uri},
response::IntoResponse,
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(
uri: Uri,
) -> Result<Response<Body>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid URI".to_string(),
)),
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(
uri: Uri,
) -> Result<Response<Body>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(
uri: Uri,
base: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else {
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}

View File

@@ -1,10 +1,11 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod handlers;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
@@ -12,38 +13,28 @@ use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
</>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -1,54 +1,41 @@
use cfg_if::cfg_if;
use leptos::{logging::log, *};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
};
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::get, Router};
use hackernews_axum::{fallback::file_and_error_handler, *};
use leptos::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
simple_logger::init_with_level(log::Level::Debug)
.expect("couldn't initialize logging");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// build our application with a route
let app = Router::new()
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
// client-only stuff for Trunk
else {
use hackernews_axum::*;
// client-only stuff for Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
use hackernews_axum::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
}
}
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(App);
}

View File

@@ -11,9 +11,8 @@ codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = [
"nightly",
"experimental-islands",
@@ -23,20 +22,20 @@ leptos_axum = { path = "../../integrations/axum", optional = true, features = [
] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true, features = ["http2"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = [
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.7", optional = true, features = ["http2"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = [
"fs",
"compression-br",
], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.11", optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
lazy_static = "1.4.0"

View File

@@ -1,44 +1,48 @@
use cfg_if::cfg_if;
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::LeptosOptions;
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(None)
});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}

View File

@@ -1,63 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -1,11 +1,11 @@
#![feature(lazy_cell)]
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fallback;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
@@ -31,16 +31,10 @@ pub fn App() -> impl IntoView {
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}
}
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
leptos::leptos_dom::HydrationCtx::stop_hydrating();
}

View File

@@ -1,16 +1,11 @@
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::{routing::get, Router};
pub use hackernews_islands::fallback::file_and_error_handler;
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
pub use axum::{routing::get, Router};
pub use hackernews_islands::fallback::file_and_error_handler;
use hackernews_islands::*;
use ssr_imports::*;
pub use leptos::get_configuration;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
@@ -26,9 +21,9 @@ async fn main() {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
logging::log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
leptos::logging::log!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
@@ -40,7 +35,5 @@ pub fn main() {
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
mount_to_body(App);
}

View File

@@ -11,22 +11,21 @@ codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
console_log = "1.0"
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.4.0", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "0.2.11", optional = true }
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.7", default-features = false, optional = true }
tower = { version = "0.4", optional = true }
http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = [
"AbortController",
"AbortSignal",
@@ -34,10 +33,10 @@ web-sys = { version = "0.3", features = [
"Response",
] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = { version = "0.4.37", features = [
wasm-bindgen-futures = { version = "0.4", features = [
"futures-core-03-stream",
], optional = true }
axum-js-fetch = { version = "0.2.1", optional = true }
axum-js-fetch = { version = "0.2", optional = true }
lazy_static = "1.4.0"
[features]

View File

@@ -1,39 +1,43 @@
use cfg_if::cfg_if;
use crate::error_template::error_template;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
//use tower::ServiceExt;
use leptos::LeptosOptions;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
//use tower::ServiceExt;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template(None));
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler =
leptos_axum::render_app_to_stream(options.to_owned(), || {
error_template(None)
});
handler(req).await.into_response()
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
_ = req;
_ = root;
todo!()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
_ = req;
_ = root;
todo!()
}

View File

@@ -1,9 +1,9 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fallback;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
@@ -29,25 +29,22 @@ pub fn App() -> impl IntoView {
}
}
cfg_if! {
if #[cfg(feature = "hydrate")] {
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
} else if #[cfg(feature = "ssr")] {
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
use axum::{
Router,
routing::post
};
use leptos_axum::{generate_route_list, LeptosRoutes};
#[cfg(feature = "ssr")]
mod ssr_imports {
use crate::App;
use axum::Router;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use log::{info, Level};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub struct Handler(axum_js_fetch::App);
@@ -58,14 +55,16 @@ cfg_if! {
console_log::init_with_level(Level::Debug);
console_error_panic_hook::set_once();
let leptos_options = LeptosOptions::builder().output_name("client").site_pkg_dir("pkg").build();
let leptos_options = LeptosOptions::builder()
.output_name("client")
.site_pkg_dir("pkg")
.build();
let routes = generate_route_list(App);
// build our application with a route
let app: axum::Router<(), axum::body::Body> = Router::new()
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.with_state(leptos_options);
let app: axum::Router = Router::new()
.leptos_routes(&leptos_options, routes, App)
.with_state(leptos_options);
info!("creating handler instance");
@@ -77,4 +76,3 @@ cfg_if! {
}
}
}
}

View File

@@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "1"
gloo-net = "0.2"
gloo-storage = "0.2"
gloo-net = "0.5"
gloo-storage = "0.3"
serde = "1.0"
thiserror = "1.0"

View File

@@ -1,5 +1,5 @@
use api_boundary::*;
use gloo_net::http::{Request, Response};
use gloo_net::http::{Request, RequestBuilder, Response};
use serde::de::DeserializeOwned;
use thiserror::Error;
@@ -41,7 +41,7 @@ impl AuthorizedApi {
fn auth_header_value(&self) -> String {
format!("Bearer {}", self.token.token)
}
async fn send<T>(&self, req: Request) -> Result<T>
async fn send<T>(&self, req: RequestBuilder) -> Result<T>
where
T: DeserializeOwned,
{

View File

@@ -5,14 +5,18 @@ edition = "2021"
publish = false
[dependencies]
api-boundary = "=0.0.0"
anyhow = "1.0"
api-boundary = "*"
axum = { version = "0.6", features = ["headers"] }
axum = "0.7"
axum-extra = { version = "0.9.2", features = ["typed-header"] }
env_logger = "0.10"
log = "0.4"
mailparse = "0.14"
pwhash = "1.0"
thiserror = "1.0"
tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.4", features = ["cors"] }
uuid = { version = "1.3", features = ["v4"] }
tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.5", features = ["cors"] }
uuid = { version = "1.6", features = ["v4"] }
parking_lot = "0.12.1"
headers = "0.4.0"

View File

@@ -1,38 +1,36 @@
use mailparse::addrparse;
use pwhash::bcrypt;
use std::{collections::HashMap, str::FromStr, sync::RwLock};
use std::{collections::HashMap, str::FromStr};
use thiserror::Error;
use uuid::Uuid;
#[derive(Default)]
pub struct AppState {
users: RwLock<HashMap<EmailAddress, Password>>,
tokens: RwLock<HashMap<Uuid, EmailAddress>>,
users: HashMap<EmailAddress, Password>,
tokens: HashMap<Uuid, EmailAddress>,
}
impl AppState {
pub fn create_user(
&self,
&mut self,
credentials: Credentials,
) -> Result<(), CreateUserError> {
let Credentials { email, password } = credentials;
let user_exists = self.users.read().unwrap().get(&email).is_some();
let user_exists = self.users.get(&email).is_some();
if user_exists {
return Err(CreateUserError::UserExists);
}
self.users.write().unwrap().insert(email, password);
self.users.insert(email, password);
Ok(())
}
pub fn login(
&self,
&mut self,
email: EmailAddress,
password: &str,
) -> Result<Uuid, LoginError> {
let valid_credentials = self
.users
.read()
.unwrap()
.get(&email)
.map(|hashed_password| hashed_password.verify(password))
.unwrap_or(false);
@@ -40,16 +38,16 @@ impl AppState {
Err(LoginError::InvalidEmailOrPassword)
} else {
let token = Uuid::new_v4();
self.tokens.write().unwrap().insert(token, email);
self.tokens.insert(token, email);
Ok(token)
}
}
pub fn logout(&self, token: &str) -> Result<(), LogoutError> {
pub fn logout(&mut self, token: &str) -> Result<(), LogoutError> {
let token = token
.parse::<Uuid>()
.map_err(|_| LogoutError::NotLoggedIn)?;
self.tokens.write().unwrap().remove(&token);
self.tokens.remove(&token);
Ok(())
}
@@ -62,8 +60,6 @@ impl AppState {
.map_err(|_| AuthError::NotAuthorized)
.and_then(|token| {
self.tokens
.read()
.unwrap()
.get(&token)
.cloned()
.map(|email| CurrentUser { email, token })

View File

@@ -1,13 +1,16 @@
use api_boundary as json;
use axum::{
extract::{State, TypedHeader},
headers::{authorization::Bearer, Authorization},
extract::State,
http::Method,
response::Json,
routing::{get, post},
Router,
};
use std::{env, sync::Arc};
use axum_extra::TypedHeader;
use headers::{authorization::Bearer, Authorization};
use parking_lot::RwLock;
use std::{env, net::SocketAddr, sync::Arc};
use tokio::net::TcpListener;
use tower_http::cors::{Any, CorsLayer};
mod adapters;
@@ -32,7 +35,7 @@ async fn main() -> anyhow::Result<()> {
}
env_logger::init();
let shared_state = Arc::new(AppState::default());
let shared_state = Arc::new(RwLock::new(AppState::default()));
let cors_layer = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
@@ -46,11 +49,10 @@ async fn main() -> anyhow::Result<()> {
.route_layer(cors_layer)
.with_state(shared_state);
let addr = "0.0.0.0:3000".parse().unwrap();
log::info!("Listen on {addr}");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
let addr = "0.0.0.0:3000".parse::<SocketAddr>()?;
log::info!("Start listening on http://{addr}");
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app.into_make_service()).await?;
Ok(())
}
@@ -73,40 +75,43 @@ enum Error {
}
async fn create_user(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
Json(credentials): Json<json::Credentials>,
) -> Result<()> {
let credentials = Credentials::try_from(credentials)?;
state.create_user(credentials)?;
state.write().create_user(credentials)?;
Ok(Json(()))
}
async fn login(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
Json(credentials): Json<json::Credentials>,
) -> Result<json::ApiToken> {
let json::Credentials { email, password } = credentials;
log::debug!("{email} tries to login");
let email = email.parse().map_err(|_|
// Here we don't want to leak detailed info.
LoginError::InvalidEmailOrPassword)?;
let token = state.login(email, &password).map(|s| s.to_string())?;
// Here we don't want to leak detailed info.
LoginError::InvalidEmailOrPassword)?;
let token = state
.write()
.login(email, &password)
.map(|s| s.to_string())?;
Ok(Json(json::ApiToken { token }))
}
async fn logout(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<()> {
state.logout(auth.token())?;
state.write().logout(auth.token())?;
Ok(Json(()))
}
async fn get_user_info(
State(state): State<Arc<AppState>>,
State(state): State<Arc<RwLock<AppState>>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<json::UserInfo> {
let user = state.authorize_user(auth.token())?;
let user = state.read().authorize_user(auth.token())?;
let CurrentUser { email, .. } = user;
Ok(Json(json::UserInfo {
email: email.into_string(),

View File

@@ -5,6 +5,10 @@ test.describe("Test Router example", () => {
await page.goto("/");
});
test("Starts on correct home page", async({ page }) => {
await expect(page.getByText("Select a contact.")).toBeVisible();
});
const links = [
{ label: "Bill Smith", url: "/0" },
{ label: "Tim Jones", url: "/1" },

View File

@@ -1,5 +1,5 @@
[package]
name = "todo_app_sqlite_viz"
name = "server_fns_axum"
version = "0.1.0"
edition = "2021"
@@ -7,48 +7,53 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos" }
leptos_viz = { path = "../../integrations/viz", optional = true }
console_log = "1.0"
console_error_panic_hook = "0.1"
futures = "0.3"
http = "1.0"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
server_fn = { path = "../../server_fn", features = ["serde-lite", "rkyv", "multipart" ]}
log = "0.4"
simple_logger = "4.0"
serde = { version = "1", features = ["derive"] }
viz = { version = "0.4.8", features = ["serve"], optional = true }
tokio = { version = "1.25.0", features = ["full"], optional = true }
http = { version = "0.2.11" }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs", "tracing", "trace"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2"
serde_toml = "0.0.1"
toml = "0.8.8"
web-sys = { version = "0.3.67", features = ["FileList", "File"] }
strum = { version = "0.25.0", features = ["strum_macros", "derive"] }
notify = { version = "6.1.1", optional = true }
pin-project-lite = "0.2.13"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:viz",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_viz",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:notify"
]
notify = ["dep:notify"]
[package.metadata.cargo-all-features]
denylist = ["viz", "tokio", "sqlx", "leptos_viz"]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_viz"
output-name = "server_fns_axum"
# 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.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
@@ -63,7 +68,8 @@ site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head

View File

@@ -0,0 +1,12 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
[tasks.test-ui]
cwd = "./e2e"
command = "cargo"
args = ["make", "test-ui", "${@}"]

View File

@@ -0,0 +1,19 @@
# Leptos Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -0,0 +1,18 @@
[package]
name = "todo_app_sqlite_axum_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0.72"
async-trait = "0.1.72"
cucumber = "0.19.1"
fantoccini = "0.19.3"
pretty_assertions = "1.4.0"
serde_json = "1.0.104"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
url = "2.4.0"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -0,0 +1,20 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"2",
"--fail-fast",
"${@}",
]

View File

@@ -0,0 +1,34 @@
# E2E Testing
This example demonstrates e2e testing with Rust using executable requirements.
## Testing Stack
| | Role | Description |
|---|---|---|
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
## Testing Organization
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
Here is a brief overview of how things fit together.
```bash
features
└── {action}_{object}.feature # Specify test scenarios
tests
├── fixtures
│ ├── action.rs # Perform a user action (click, type, etc.)
│ ├── check.rs # Assert what a user can see/not see
│ ├── find.rs # Query page elements
│ ├── mod.rs
│ └── world
│ ├── action_steps.rs # Map Gherkin steps to user actions
│ ├── check_steps.rs # Map Gherkin steps to user expectations
│ └── mod.rs
└── app_suite.rs # Test main
```

View File

@@ -0,0 +1,16 @@
@add_todo
Feature: Add Todo
Background:
Given I see the app
@add_todo-see
Scenario: Should see the todo
Given I set the todo as Buy Bread
When I click the Add button
Then I see the todo named Buy Bread
@add_todo-style
Scenario: Should see the pending todo
When I add a todo as Buy Oranges
Then I see the pending todo

View File

@@ -0,0 +1,18 @@
@delete_todo
Feature: Delete Todo
Background:
Given I see the app
@serial
@delete_todo-remove
Scenario: Should not see the deleted todo
Given I add a todo as Buy Yogurt
When I delete the todo named Buy Yogurt
Then I do not see the todo named Buy Yogurt
@serial
@delete_todo-message
Scenario: Should see the empty list message
When I empty the todo list
Then I see the empty list message is No tasks were found.

View File

@@ -0,0 +1,12 @@
@open_app
Feature: Open App
@open_app-title
Scenario: Should see the home page title
When I open the app
Then I see the page title is My Tasks
@open_app-label
Scenario: Should see the input label
When I open the app
Then I see the label of the input is Add a Todo

View File

@@ -0,0 +1,14 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
#[tokio::main]
async fn main() -> Result<()> {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit("./features")
.await;
Ok(())
}

View File

@@ -0,0 +1,60 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
use tokio::{self, time};
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn add_todo(client: &Client, text: &str) -> Result<()> {
fill_todo(client, text).await?;
click_add_button(client).await?;
Ok(())
}
pub async fn fill_todo(client: &Client, text: &str) -> Result<()> {
let textbox = find::todo_input(client).await;
textbox.send_keys(text).await?;
Ok(())
}
pub async fn click_add_button(client: &Client) -> Result<()> {
let add_button = find::add_button(client).await;
add_button.click().await?;
Ok(())
}
pub async fn empty_todo_list(client: &Client) -> Result<()> {
let todos = find::todos(client).await;
for _todo in todos {
let _ = delete_first_todo(client).await?;
}
Ok(())
}
pub async fn delete_first_todo(client: &Client) -> Result<()> {
if let Some(element) = find::first_delete_button(client).await {
element.click().await.expect("Failed to delete todo");
time::sleep(time::Duration::from_millis(250)).await;
}
Ok(())
}
pub async fn delete_todo(client: &Client, text: &str) -> Result<()> {
if let Some(element) = find::delete_button(client, text).await {
element.click().await?;
time::sleep(time::Duration::from_millis(250)).await;
}
Ok(())
}

View File

@@ -0,0 +1,57 @@
use super::find;
use anyhow::{Ok, Result};
use fantoccini::{Client, Locator};
use pretty_assertions::assert_eq;
pub async fn text_on_element(
client: &Client,
selector: &str,
expected_text: &str,
) -> Result<()> {
let element = client
.wait()
.for_element(Locator::Css(selector))
.await
.expect(
format!("Element not found by Css selector `{}`", selector)
.as_str(),
);
let actual = element.text().await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn todo_present(
client: &Client,
text: &str,
expected: bool,
) -> Result<()> {
let todo_present = is_todo_present(client, text).await;
assert_eq!(todo_present, expected);
Ok(())
}
async fn is_todo_present(client: &Client, text: &str) -> bool {
let todos = find::todos(client).await;
for todo in todos {
let todo_title = todo.text().await.expect("Todo title not found");
if todo_title == text {
return true;
}
}
false
}
pub async fn todo_is_pending(client: &Client) -> Result<()> {
if let None = find::pending_todo(client).await {
assert!(false, "Pending todo not found");
}
Ok(())
}

View File

@@ -0,0 +1,63 @@
use fantoccini::{elements::Element, Client, Locator};
pub async fn todo_input(client: &Client) -> Element {
let textbox = client
.wait()
.for_element(Locator::Css("input[name='title"))
.await
.expect("Todo textbox not found");
textbox
}
pub async fn add_button(client: &Client) -> Element {
let button = client
.wait()
.for_element(Locator::Css("input[value='Add']"))
.await
.expect("");
button
}
pub async fn first_delete_button(client: &Client) -> Option<Element> {
if let Ok(element) = client
.wait()
.for_element(Locator::Css("li:first-child input[value='X']"))
.await
{
return Some(element);
}
None
}
pub async fn delete_button(client: &Client, text: &str) -> Option<Element> {
let selector = format!("//*[text()='{text}']//input[@value='X']");
if let Ok(element) =
client.wait().for_element(Locator::XPath(&selector)).await
{
return Some(element);
}
None
}
pub async fn pending_todo(client: &Client) -> Option<Element> {
if let Ok(element) =
client.wait().for_element(Locator::Css(".pending")).await
{
return Some(element);
}
None
}
pub async fn todos(client: &Client) -> Vec<Element> {
let todos = client
.find_all(Locator::Css("li"))
.await
.expect("Todo List not found");
todos
}

View File

@@ -0,0 +1,4 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -0,0 +1,57 @@
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[given(regex = "^I add a todo as (.*)$")]
#[when(regex = "^I add a todo as (.*)$")]
async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::add_todo(client, text.as_str()).await?;
Ok(())
}
#[given(regex = "^I set the todo as (.*)$")]
async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::fill_todo(client, &text).await?;
Ok(())
}
#[when(regex = "I click the Add button$")]
async fn i_click_the_button(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::click_add_button(client).await?;
Ok(())
}
#[when(regex = "^I delete the todo named (.*)$")]
async fn i_delete_the_todo_named(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
action::delete_todo(client, text.as_str()).await?;
Ok(())
}
#[given("the todo list is empty")]
#[when("I empty the todo list")]
async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::empty_todo_list(client).await?;
Ok(())
}

View File

@@ -0,0 +1,67 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = "^I see the page title is (.*)$")]
async fn i_see_the_page_title_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "h1", &text).await?;
Ok(())
}
#[then(regex = "^I see the label of the input is (.*)$")]
async fn i_see_the_label_of_the_input_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "label", &text).await?;
Ok(())
}
#[then(regex = "^I see the todo named (.*)$")]
async fn i_see_the_todo_is_present(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::todo_present(client, text.as_str(), true).await?;
Ok(())
}
#[then("I see the pending todo")]
async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::todo_is_pending(client).await?;
Ok(())
}
#[then(regex = "^I see the empty list message is (.*)$")]
async fn i_see_the_empty_list_message_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "ul p", &text).await?;
Ok(())
}
#[then(regex = "^I do not see the todo named (.*)$")]
async fn i_do_not_see_the_todo_is_present(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::todo_present(client, text.as_str(), false).await?;
Ok(())
}

View File

@@ -0,0 +1,39 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,508 @@
use futures::StreamExt;
use leptos::{html::Input, *};
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
use leptos_router::{ActionForm, Route, Router, Routes};
use server_fn::codec::{
GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText,
TextStream,
};
#[cfg(feature = "ssr")]
use std::sync::{
atomic::{AtomicU8, Ordering},
Mutex,
};
use strum::{Display, EnumString};
use wasm_bindgen::JsCast;
use web_sys::{FormData, HtmlFormElement, SubmitEvent};
#[component]
pub fn TodoApp() -> impl IntoView {
provide_meta_context();
view! {
<Meta name="color-scheme" content="dark light"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/server_fns_axum.css"/>
<Router>
<header>
<h1>"Server Function Demo"</h1>
</header>
<main>
<Routes>
<Route path="" view=HomePage/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<h2>"Some Simple Server Functions"</h2>
<SpawnLocal/>
<WithAnAction/>
<WithActionForm/>
<h2>"Custom Error Types"</h2>
<CustomErrorTypes/>
<h2>"Alternative Encodings"</h2>
<ServerFnArgumentExample/>
<RkyvExample/>
<FileUpload/>
<FileWatcher/>
}
}
/// A server function is really just an API call to your server. But it provides a plain async
/// function as a wrapper around that. This means you can call it like any other async code, just
/// by spawning a task with `spawn_local`.
///
/// In reality, you usually want to use a resource to load data from the server or an action to
/// mutate data on the server. But a simple `spawn_local` can make it more obvious what's going on.
#[component]
pub fn SpawnLocal() -> impl IntoView {
/// A basic server function can be called like any other async function.
///
/// You can define a server function at any scope. This one, for example, is only available
/// inside the SpawnLocal component. **However**, note that all server functions are publicly
/// available API endpoints: This scoping means you can only call this server function
/// from inside this component, but it is still available at its URL to any caller, from within
/// your app or elsewhere.
#[server]
pub async fn shouting_text(input: String) -> Result<String, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.to_ascii_uppercase())
}
let input_ref = NodeRef::<Input>::new();
let (shout_result, set_shout_result) =
create_signal("Click me".to_string());
view! {
<h3>Using <code>spawn_local</code></h3>
<p>
"You can call a server function by using "<code>"spawn_local"</code> " in an event listener. "
"Clicking this button should alert with the uppercase version of the input."
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let uppercase_text = shouting_text(value).await.unwrap_or_else(|e| e.to_string());
set_shout_result(uppercase_text);
});
}
>
{shout_result}
</button>
}
}
/// Pretend this is a database and we're storing some rows in memory!
/// This exists only on the server.
#[cfg(feature = "ssr")]
static ROWS: Mutex<Vec<String>> = Mutex::new(Vec::new());
/// Imagine this server function mutates some state on the server, like a database row.
/// Every third time, it will return an error.
///
/// This kind of mutation is often best handled by an Action.
/// Remember, if you're loading data, use a resource; if you're running an occasional action,
/// use an action.
#[server]
pub async fn add_row(text: String) -> Result<usize, ServerFnError> {
static N: AtomicU8 = AtomicU8::new(0);
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
let nth_run = N.fetch_add(1, Ordering::Relaxed);
// this will print on the server, like any server function
println!("Adding {text:?} to the database!");
if nth_run % 3 == 2 {
Err(ServerFnError::new("Oh no! Couldn't add to database!"))
} else {
let mut rows = ROWS.lock().unwrap();
rows.push(text);
Ok(rows.len())
}
}
/// Simply returns the number of rows.
#[server]
pub async fn get_rows() -> Result<usize, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(ROWS.lock().unwrap().len())
}
/// An action abstracts over the process of spawning a future and setting a signal when it
/// resolves. Its .input() signal holds the most recent argument while it's still pending,
/// and its .value() signal holds the most recent result. Its .version() signal can be fed
/// into a resource, telling it to refetch whenever the action has successfully resolved.
///
/// This makes actions useful for mutations, i.e., some server function that invalidates
/// loaded previously loaded from another server function.
#[component]
pub fn WithAnAction() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
// a server action can be created by using the server function's type name as a generic
// the type name defaults to the PascalCased function name
let action = create_server_action::<AddRow>();
// this resource will hold the total number of rows
// passing it action.version() means it will refetch whenever the action resolves successfully
let row_count = create_resource(action.version(), |_| get_rows());
view! {
<h3>Using <code>create_action</code></h3>
<p>
"Some server functions are conceptually \"mutations,\", which change something on the server. "
"These often work well as actions."
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let text = input_ref.get().unwrap().value();
action.dispatch(text);
// note: technically, this `action` takes `AddRow` (the server fn type) as its
// argument
//
// however, `.dispatch()` takes `impl Into<I>`, and for any one-argument server
// functions, `From<_>` is implemented between the server function type and the
// type of this single argument
//
// so `action.dispatch(text)` means `action.dispatch(AddRow { text })`
}
>
Submit
</button>
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
<Transition>
<p>Total rows: {row_count}</p>
</Transition>
}
}
/// An <ActionForm/> lets you do the same thing as dispatching an action, but automates the
/// creation of the dispatched argument struct using a <form>. This means it also gracefully
/// degrades well when JS/WASM are not available.
///
/// Try turning off WASM in your browser. The form still works, and successfully displays the error
/// message if the server function returns an error. Otherwise, it loads the new resource data.
#[component]
pub fn WithActionForm() -> impl IntoView {
let action = create_server_action::<AddRow>();
let row_count = create_resource(action.version(), |_| get_rows());
view! {
<h3>Using <code>"<ActionForm/>"</code></h3>
<p>
<code>"<ActionForm/>"</code> "lets you use an HTML " <code>"<form>"</code>
"to call a server function in a way that gracefully degrades."
</p>
<ActionForm action>
<input
// the `name` of the input corresponds to the argument name
name="text"
placeholder="Type something here."
/>
<button> Submit </button>
</ActionForm>
<p>You submitted: {move || format!("{:?}", action.input().get())}</p>
<p>The result was: {move || format!("{:?}", action.value().get())}</p>
<Transition>archive underaligned: need alignment 4 but have alignment 1
<p>Total rows: {row_count}</p>
</Transition>
}
}
/// The plain `#[server]` macro gives sensible defaults for the settings needed to create a server
/// function, but those settings can also be customized. For example, you can set a specific unique
/// path rather than the hashed path, or you can choose a different combination of input and output
/// encodings.
///
/// Arguments to the server macro can be specified as named key-value pairs, like `name = value`.
#[server(
// this server function will be exposed at /api2/custom_path
prefix = "/api2",
endpoint = "custom_path",
// it will take its arguments as a URL-encoded GET request (useful for caching)
input = GetUrl,
// it will return its output using SerdeLite
// (this needs to be enabled with the `serde-lite` feature on the `server_fn` crate
output = SerdeLite,
)]
// You can use the `#[middleware]` macro to add appropriate middleware
// In this case, any `tower::Layer` that takes services of `Request<Body>` will work
#[middleware(crate::middleware::LoggingLayer)]
pub async fn length_of_input(input: String) -> Result<usize, ServerFnError> {
println!("2. Running server function.");
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.len())
}
#[component]
pub fn ServerFnArgumentExample() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal(0);
view! {
<h3>Custom arguments to the <code>#[server]</code> " macro"</h3>
<p>
This example shows how to specify additional behavior including
<ul>
<li>Specific server function <strong>paths</strong></li>
<li>Mixing and matching input and output <strong>encodings</strong></li>
<li>Adding custom <strong>middleware</strong> on a per-server-fn basis</li>
</ul>
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let length = length_of_input(value).await.unwrap_or(0);
set_result(length);
});
}
>
Click to see length
</button>
<p>Length is {result}</p>
}
}
/// `server_fn` supports a wide variety of input and output encodings, each of which can be
/// referred to as a PascalCased struct name
/// - Toml
/// - Cbor
/// - Rkyv
/// - etc.
#[server(
input = Rkyv,
output = Rkyv
)]
pub async fn rkyv_example(input: String) -> Result<String, ServerFnError> {
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.to_ascii_uppercase())
}
#[component]
pub fn RkyvExample() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (input, set_input) = create_signal(String::new());
let rkyv_result = create_resource(input, rkyv_example);
view! {
<h3>Using <code>rkyv</code> encoding</h3>
<p>
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
set_input(value);
}
>
Click to see length
</button>
<p>{input}</p>
<Transition>
{rkyv_result}
</Transition>
}
}
#[component]
pub fn FileUpload() -> impl IntoView {
/// A simple file upload function, which does just returns the length of the file.
///
/// On the server, this uses the `multer` crate, which provides a streaming API.
#[server(
input = MultipartFormData,
)]
pub async fn file_length(
data: MultipartData,
) -> Result<usize, ServerFnError> {
// `.into_inner()` returns the inner `multer` stream
// it is `None` if we call this on the client, but always `Some(_)` on the server, so is safe to
// unwrap
let mut data = data.into_inner().unwrap();
// this will just measure the total number of bytes uploaded
let mut count = 0;
while let Ok(Some(mut field)) = data.next_field().await {
println!("\n[NEXT FIELD]\n");
let name = field.name().unwrap_or_default().to_string();
println!(" [NAME] {name}");
while let Ok(Some(chunk)) = field.chunk().await {
let len = chunk.len();
count += len;
println!(" [CHUNK] {len}");
// in a real server function, you'd do something like saving the file here
}
}
Ok(count)
}
let upload_action = create_action(|data: &FormData| {
let data = data.clone();
// `MultipartData` implements `From<FormData>`
file_length(data.into())
});
view! {
<h3>File Upload</h3>
<p>Uploading files is fairly easy using multipart form data.</p>
<form on:submit=move |ev: SubmitEvent| {
ev.prevent_default();
let target = ev.target().unwrap().unchecked_into::<HtmlFormElement>();
let form_data = FormData::new_with_form(&target).unwrap();
upload_action.dispatch(form_data);
}>
<input type="file" name="file_to_upload"/>
<input type="submit"/>
</form>
<p>
{move || if upload_action.input().get().is_none() && upload_action.value().get().is_none() {
"Upload a file.".to_string()
} else if upload_action.pending().get() {
"Uploading...".to_string()
} else if let Some(Ok(value)) = upload_action.value().get() {
value.to_string()
} else {
format!("{:?}", upload_action.value().get())
}}
</p>
}
}
#[component]
pub fn FileWatcher() -> impl IntoView {
#[server(input = GetUrl, output = StreamingText)]
pub async fn watched_files() -> Result<TextStream, ServerFnError> {
use notify::{
Config, Error, Event, RecommendedWatcher, RecursiveMode, Watcher,
};
use std::path::Path;
let (tx, rx) = futures::channel::mpsc::unbounded();
let mut watcher = RecommendedWatcher::new(
move |res: Result<Event, Error>| {
if let Ok(ev) = res {
if let Some(path) = ev.paths.last() {
let filename = path
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
_ = tx.unbounded_send(filename); //res);
}
}
},
Config::default(),
)?;
watcher
.watch(Path::new("./watched_files"), RecursiveMode::Recursive)?;
std::mem::forget(watcher);
Ok(TextStream::from(rx))
}
let (files, set_files) = create_signal(Vec::new());
create_effect(move |_| {
spawn_local(async move {
while let Some(res) =
watched_files().await.unwrap().into_inner().next().await
{
if let Ok(filename) = res {
set_files.update(|n| n.push(filename));
}
}
});
});
view! {
<h3>Watching files and returning a streaming response</h3>
<p>Files changed since you loaded the page:</p>
<ul>
{move || files.get().into_iter().map(|file| view! { <li><code>{file}</code></li> }).collect::<Vec<_>>()}
</ul>
<p><em>Add or remove some text files in the <code>watched_files</code> directory and see the list of changes here.</em></p>
}
}
/// The `ServerFnError` type is generic over a custom error type, which defaults to `NoCustomError`
/// for backwards compatibility and to support the most common use case.
///
/// A custom error type should implement `FromStr` and `Display`, which allows it to be converted
/// into and from a string easily to be sent over the network. It does *not* need to implement
/// `Serialize` and `Deserialize`, although these can be used to generate the `FromStr`/`Display`
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
/// simply to generate those trait implementations.
#[server]
pub async fn ascii_uppercase(
text: String,
) -> Result<String, ServerFnError<InvalidArgument>> {
if text.len() < 5 {
Err(InvalidArgument::TooShort.into())
} else if text.len() > 15 {
Err(InvalidArgument::TooLong.into())
} else if text.is_ascii() {
Ok(text.to_ascii_uppercase())
} else {
Err(InvalidArgument::NotAscii.into())
}
}
// The EnumString and Display derive macros are provided by strum
#[derive(Debug, Clone, EnumString, Display)]
pub enum InvalidArgument {
TooShort,
TooLong,
NotAscii,
}
#[component]
pub fn CustomErrorTypes() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
let (result, set_result) = create_signal(None);
view! {
<h3>Using custom error types</h3>
<p>
"Server functions can use a custom error type that is preserved across the network boundary."
</p>
<p>
"Try typing a message that is between 5 and 15 characters of ASCII text below. Then try breaking \
the rules!"
</p>
<input node_ref=input_ref placeholder="Type something here."/>
<button
on:click=move |_| {
let value = input_ref.get().unwrap().value();
spawn_local(async move {
let data = ascii_uppercase(value).await;
set_result(Some(data));
});
}
>
"Submit"
</button>
<p>
{move || format!("{:?}", result.get())}
</p>
}
}

View File

@@ -1,8 +1,7 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_viz::ResponseOptions;
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
@@ -24,18 +23,17 @@ pub fn ErrorTemplate(
let errors: Vec<TodoAppError> = errors
.get()
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! {
if #[cfg(feature="ssr")]{
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response{
response.set_status(errors[0].status_code());
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
}
view! {
@@ -46,11 +44,10 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children= move |error| {
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}

View File

@@ -0,0 +1,50 @@
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
);
handler(req).await.into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -0,0 +1,17 @@
pub mod app;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod middleware;
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::TodoApp;
_ = console_log::init_with_level(log::Level::Error);
console_error_panic_hook::set_once();
leptos::mount_to_body(TodoApp);
}

View File

@@ -0,0 +1,31 @@
use crate::{app::*, fallback::file_and_error_handler};
use axum::Router;
use leptos::{get_configuration, logging};
use leptos_axum::{generate_route_list, LeptosRoutes};
use server_fns_axum::*;
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
// build our application with a route
let app = Router::new()
.leptos_routes(&leptos_options, routes, TodoApp)
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}

View File

@@ -0,0 +1,72 @@
use axum::body::Body;
use http::Request;
use pin_project_lite::pin_project;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{Layer, Service};
pub struct LoggingLayer;
impl<S> Layer<S> for LoggingLayer {
type Service = LoggingService<S>;
fn layer(&self, inner: S) -> Self::Service {
LoggingService { inner }
}
}
pub struct LoggingService<T> {
inner: T,
}
impl<T> Service<Request<Body>> for LoggingService<T>
where
T: Service<Request<Body>>,
{
type Response = T::Response;
type Error = T::Error;
type Future = LoggingServiceFuture<T::Future>;
fn poll_ready(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
println!("1. Running my middleware!");
LoggingServiceFuture {
inner: self.inner.call(req),
}
}
}
pin_project! {
pub struct LoggingServiceFuture<T> {
#[pin]
inner: T,
}
}
impl<T> Future for LoggingServiceFuture<T>
where
T: Future,
{
type Output = T::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.inner.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(output) => {
println!("3. Running my middleware!");
Poll::Ready(output)
}
}
}
}

View File

@@ -1,3 +1,3 @@
.pending {
color: purple;
}
}

View File

@@ -7,40 +7,40 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "1.0.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
anyhow = "1.0"
console_log = "1.0"
rand = { version = "0.8", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1"
futures = "0.3"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
axum = { version = "0.6.1", optional = true, features=["macros"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.11" }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1.0", features = ["derive"] }
axum = { version = "0.7", optional = true, features=["macros"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
http = { version = "1.0" }
sqlx = { version = "0.7.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
thiserror = "1.0"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.9.0", features = [
axum_session_auth = { version = "0.10", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.9.0", features = [
axum_session = { version = "0.10", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.14", optional = true }
async-trait = { version = "0.1.64", optional = true }
bcrypt = { version = "0.15", optional = true }
async-trait = { version = "0.1", optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",

Binary file not shown.

View File

@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -38,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
"lastModified": 1685573264,
"narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0874168639713f547c05947c76124f78441ea46c",
"rev": "380be19fbd2d9079f677978361792cb25e8a3635",
"type": "github"
},
"original": {
@@ -67,11 +67,11 @@
]
},
"locked": {
"lastModified": 1681525152,
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
"lastModified": 1703902408,
"narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
"rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
"type": "github"
},
"original": {

View File

@@ -1,17 +1,7 @@
use cfg_if::cfg_if;
use leptos::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
use bcrypt::{hash, verify, DEFAULT_COST};
use crate::todo::{pool, auth};
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
}}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct User {
pub id: i64,
@@ -33,17 +23,33 @@ impl Default for User {
}
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use async_trait::async_trait;
#[cfg(feature = "ssr")]
pub mod ssr {
pub use super::User;
pub use axum_session_auth::{
Authentication, HasPermission, SessionSqlitePool,
};
pub use sqlx::SqlitePool;
pub use std::collections::HashSet;
pub type AuthSession = axum_session_auth::AuthSession<
User,
i64,
SessionSqlitePool,
SqlitePool,
>;
pub use crate::todo::ssr::{auth, pool};
pub use async_trait::async_trait;
pub use bcrypt::{hash, verify, DEFAULT_COST};
impl User {
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
.bind(id)
.fetch_one(pool)
.await
.ok()?;
let sqluser = sqlx::query_as::<_, SqlUser>(
"SELECT * FROM users WHERE id = ?",
)
.bind(id)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
@@ -57,12 +63,17 @@ if #[cfg(feature = "ssr")] {
Some(sqluser.into_user(Some(sql_user_perms)))
}
pub async fn get_from_username(name: String, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE username = ?")
.bind(name)
.fetch_one(pool)
.await
.ok()?;
pub async fn get_from_username(
name: String,
pool: &SqlitePool,
) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>(
"SELECT * FROM users WHERE username = ?",
)
.bind(name)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
@@ -84,7 +95,10 @@ if #[cfg(feature = "ssr")] {
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
async fn load_user(
userid: i64,
pool: Option<&SqlitePool>,
) -> Result<User, anyhow::Error> {
let pool = pool.unwrap();
User::get(userid, pool)
@@ -120,7 +134,10 @@ if #[cfg(feature = "ssr")] {
}
impl SqlUser {
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
pub fn into_user(
self,
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
) -> User {
User {
id: self.id,
username: self.username,
@@ -137,15 +154,16 @@ if #[cfg(feature = "ssr")] {
}
}
}
}
#[server(Foo, "/api")]
#[server]
pub async fn foo() -> Result<String, ServerFnError> {
Ok(String::from("Bar!"))
}
#[server(GetUser, "/api")]
#[server]
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
use crate::todo::ssr::auth;
let auth = auth()?;
Ok(auth.current_user)
@@ -157,14 +175,14 @@ pub async fn login(
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
let user: User = User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError("User does not exist.".into())
})?;
.ok_or_else(|| ServerFnError::new("User does not exist."))?;
match verify(password, &user.password)? {
true => {
@@ -186,6 +204,8 @@ pub async fn signup(
password_confirmation: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
use self::ssr::*;
let pool = pool()?;
let auth = auth()?;
@@ -207,9 +227,7 @@ pub async fn signup(
User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError(
"Signup failed: User does not exist.".into(),
)
ServerFnError::new("Signup failed: User does not exist.")
})?;
auth.login_user(user.id);
@@ -222,6 +240,8 @@ pub async fn signup(
#[server(Logout, "/api")]
pub async fn logout() -> Result<(), ServerFnError> {
use self::ssr::*;
let auth = auth()?;
auth.logout_user();

View File

@@ -1,5 +1,4 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -29,13 +28,12 @@ pub fn ErrorTemplate(
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! {
if #[cfg(feature="ssr")]{
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response{
response.set_status(errors[0].status_code());
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
}
view! {

View File

@@ -1,47 +1,50 @@
use cfg_if::cfg_if;
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{<ErrorTemplate outside_errors=errors.clone()/>});
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}
}

View File

@@ -1,27 +1,18 @@
use cfg_if::cfg_if;
pub mod auth;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod state;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
use leptos::view;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::todo::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|| {
view! { <TodoApp/> }
});
}
}
leptos::mount_to_body(TodoApp);
}

View File

@@ -1,117 +1,131 @@
use cfg_if::cfg_if;
use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{IntoResponse, Response},
routing::get,
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
use leptos::{get_configuration, logging::log, provide_context};
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use session_auth_axum::{
auth::{ssr::AuthSession, User},
fallback::file_and_error_handler,
state::AppState,
todo::*,
};
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::get,
extract::{Path, State, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
};
use session_auth_axum::todo::*;
use session_auth_axum::auth::*;
use session_auth_axum::state::AppState;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{logging::log, provide_context, get_configuration};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
async fn server_fn_handler(
State(app_state): State<AppState>,
auth_session: AuthSession,
path: Path<String>,
request: Request<AxumBody>,
) -> impl IntoResponse {
log!("{:?}", path);
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
request: Request<AxumBody>) -> impl IntoResponse {
log!("{:?}", path);
handle_server_fns_with_context(path, headers, raw_query, move || {
handle_server_fns_with_context(
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
}, request).await
},
request,
)
.await
}
async fn leptos_routes_handler(
auth_session: AuthSession,
State(app_state): State<AppState>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_route_with_context(
app_state.leptos_options.clone(),
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
TodoApp,
);
handler(req).await.into_response()
}
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info)
.expect("couldn't initialize logging");
let pool = SqlitePoolOptions::new()
.connect("sqlite:Todos.db")
.await
.expect("Could not make pool.");
// Auth section
let session_config =
SessionConfig::default().with_table_name("axum_sessions");
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(
Some(pool.clone().into()),
session_config,
)
.await
.unwrap();
if let Err(e) = sqlx::migrate!().run(&pool).await {
eprintln!("{e:?}");
}
async fn leptos_routes_handler(auth_session: AuthSession, State(app_state): State<AppState>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_route_with_context(app_state.leptos_options.clone(),
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
TodoApp
);
handler(req).await.into_response()
}
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetTodos::register();
// _ = AddTodo::register();
// _ = DeleteTodo::register();
// _ = Login::register();
// _ = Logout::register();
// _ = Signup::register();
// _ = GetUser::register();
// _ = Foo::register();
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let pool = SqlitePoolOptions::new()
.connect("sqlite:Todos.db")
.await
.expect("Could not make pool.");
let app_state = AppState {
leptos_options,
pool: pool.clone(),
routes: routes.clone(),
};
// Auth section
let session_config = SessionConfig::default().with_table_name("axum_sessions");
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
sqlx::migrate!()
.run(&pool)
.await
.expect("could not run SQLx migrations");
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetTodos::register();
// _ = AddTodo::register();
// _ = DeleteTodo::register();
// _ = Login::register();
// _ = Logout::register();
// _ = Signup::register();
// _ = GetUser::register();
// _ = Foo::register();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let app_state = AppState{
leptos_options,
pool: pool.clone(),
routes: routes.clone(),
};
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
// build our application with a route
let app = Router::new()
.route(
"/api/*fn_name",
get(server_fn_handler).post(server_fn_handler),
)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
.with_config(auth_config))
.layer(
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
Some(pool.clone()),
)
.with_config(auth_config),
)
.layer(SessionLayer::new(session_store))
.with_state(app_state);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// Only the server may directly connect to the database.
}
}
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}

View File

@@ -1,18 +1,13 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos::LeptosOptions;
use sqlx::SqlitePool;
use axum::extract::FromRef;
use leptos::LeptosOptions;
use leptos_router::RouteListing;
use sqlx::SqlitePool;
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
#[derive(FromRef, Debug, Clone)]
pub struct AppState{
pub struct AppState {
pub leptos_options: LeptosOptions,
pub pool: SqlitePool,
pub routes: Vec<RouteListing>,
}
}
}

View File

@@ -1,5 +1,4 @@
use crate::{auth::*, error_template::ErrorTemplate};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -14,40 +13,41 @@ pub struct Todo {
completed: bool,
}
cfg_if! {
if #[cfg(feature = "ssr")] {
#[cfg(feature = "ssr")]
pub mod ssr {
use super::Todo;
use crate::auth::{ssr::AuthSession, User};
use leptos::*;
use sqlx::SqlitePool;
use sqlx::SqlitePool;
use futures::future::join_all;
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn pool() -> Result<SqlitePool, ServerFnError> {
use_context::<SqlitePool>()
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>().ok_or_else(|| {
ServerFnError::ServerError("Auth session missing.".into())
})
}
pub fn auth() -> Result<AuthSession, ServerFnError> {
use_context::<AuthSession>()
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlTodo {
id: u32,
user_id: i64,
title: String,
created_at: String,
completed: bool,
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlTodo {
id: u32,
user_id: i64,
title: String,
created_at: String,
completed: bool,
}
impl SqlTodo {
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
Todo {
id: self.id,
user: User::get(self.user_id, pool).await,
title: self.title,
created_at: self.created_at,
completed: self.completed,
}
impl SqlTodo {
pub async fn into_todo(self, pool: &SqlitePool) -> Todo {
Todo {
id: self.id,
user: User::get(self.user_id, pool).await,
title: self.title,
created_at: self.created_at,
completed: self.completed,
}
}
}
@@ -55,6 +55,9 @@ cfg_if! {
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
use self::ssr::{pool, SqlTodo};
use futures::future::join_all;
let pool = pool()?;
Ok(join_all(
@@ -69,6 +72,8 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
use self::ssr::*;
let user = get_user().await?;
let pool = pool()?;
@@ -93,6 +98,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
use self::ssr::*;
let pool = pool()?;
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")

View File

@@ -27,16 +27,16 @@ tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
sqlx = { version = "0.6.2", features = [
sqlx = { version = "0.7", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.2.1", features = [
axum_session_auth = { version = "0.9", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.2.3", features = [
axum_session = { version = "0.9", features = [
"sqlite-rustls",
], optional = true }
async-trait = { version = "0.1.64", optional = true }

View File

@@ -69,8 +69,7 @@ if #[cfg(feature = "ssr")] {
.with_security_mode(SecurityMode::PerSession);
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
session_store.initiate().await.unwrap();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
sqlx::migrate!()
.run(&pool)

View File

@@ -11,7 +11,6 @@ actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }

View File

@@ -9,13 +9,12 @@ use thiserror::Error;
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
view! {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>
<Router fallback>
<Router>
<main>
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>

View File

@@ -1,25 +1,15 @@
#![feature(result_flattening)]
pub mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
use wasm_bindgen::prelude::wasm_bindgen;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
use leptos::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
leptos::mount_to_body(App);
}

View File

@@ -23,12 +23,7 @@ async fn main() -> std::io::Result<()> {
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|| view! { <App/> },
)
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})

View File

@@ -9,7 +9,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
@@ -18,9 +17,9 @@ leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
thiserror = "1"
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1", features = ["time"], optional = true }
wasm-bindgen = "0.2"

View File

@@ -9,13 +9,12 @@ use thiserror::Error;
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
view! {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>
<Router fallback>
<Router>
<main>
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>

View File

@@ -1,43 +1,48 @@
use cfg_if::cfg_if;
use crate::app::App;
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions, view};
use crate::app::App;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view!{ <App/> }
);
handler(req).await.into_response()
}
if res.status() == StatusCode::OK {
res.into_response()
} else {
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! { <App/> },
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}}
}

View File

@@ -1,26 +1,18 @@
#![feature(result_flattening)]
pub mod app;
#[cfg(feature = "ssr")]
pub mod fallback;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
use wasm_bindgen::prelude::wasm_bindgen;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
use leptos::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
leptos::mount_to_body(App);
}

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::post, Router};
use axum::Router;
use leptos::{logging::log, *};
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
@@ -19,7 +19,6 @@ async fn main() {
// _ = ListPostMetadata::register();
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.fallback(file_and_error_handler)
.with_state(leptos_options);
@@ -27,8 +26,8 @@ async fn main() {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}

View File

@@ -11,7 +11,6 @@ actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["serde"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }

View File

@@ -1,23 +1,12 @@
pub mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
use wasm_bindgen::prelude::wasm_bindgen;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
use leptos::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|| {
view! { <App/> }
});
}
}
leptos::mount_to_body(App);
}

View File

@@ -10,19 +10,14 @@ async fn main() -> std::io::Result<()> {
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|| view! { <App/> });
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|| view! { <App/> },
)
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})

View File

@@ -16,7 +16,6 @@ leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
gloo-net = { version = "0.2", features = ["http"] }
log = "0.4"
cfg-if = "1.0"
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }

View File

@@ -1,39 +1,27 @@
mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::app::*;
use leptos::*;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::App;
use leptos::{logging, mount_to_body};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
logging::log!("hydrate mode - hydrating");
logging::log!("hydrate mode - hydrating");
leptos::mount_to_body(|| {
view! { <App/> }
});
}
}
else if #[cfg(feature = "csr")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(start)]
pub fn main() {
use app::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
logging::log!("csr mode - mounting to body");
mount_to_body(|| {
view! { <App /> }
});
}
}
mount_to_body(App);
}
#[cfg(feature = "csr")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn main() {
use crate::app::App;
use leptos::{logging, mount_to_body};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
logging::log!("csr mode - mounting to body");
mount_to_body(App);
}

View File

@@ -1,40 +1,35 @@
mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use actix_files::Files;
use actix_web::*;
use leptos::*;
use crate::app::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use crate::app::*;
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|| view! { <App/> });
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|| view! { <App/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let routes = &routes;
App::new()
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <App/> })
.service(Files::new("/", site_root))
.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
}
else {
pub fn main() {}
}
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
let routes = &routes;
App::new()
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|| view! { <App/> },
)
.service(Files::new("/", site_root))
.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}

View File

@@ -7,23 +7,22 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.6.18", optional = true }
console_error_panic_hook = "0.1.7"
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4"
tokio = { version = "1.28.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
wasm-bindgen = "0.2.84"
thiserror = "1.0.40"
tracing = { version = "0.1.37", optional = true }
http = "0.2.11"
tokio = { version = "1", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "0.2"
thiserror = "1.0"
tracing = { version = "0.1", optional = true }
http = "1.0"
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]

Some files were not shown because too many files have changed in this diff Show More