Compare commits

..

3 Commits

Author SHA1 Message Date
Greg Johnston
2dd8b78336 v0.3.1 2023-06-14 09:56:41 -04:00
Greg Johnston
fd901d3e53 tests: fix broken SSR doctests (#1056) 2023-06-14 09:14:13 -04:00
Nova
d517f01e58 fix: replace ouroboros with self_cell (#1171) 2023-06-14 08:15:06 -04:00
43 changed files with 171 additions and 424 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.3.0"
version = "0.3.1"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
leptos_router = { path = "./router", version = "0.3.0" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
leptos = { path = "./leptos", default-features = false, version = "0.3.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.1" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.1" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.1" }
leptos_router = { path = "./router", version = "0.3.1" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.1" }
[profile.release]
codegen-units = 1

View File

@@ -41,14 +41,6 @@ build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]
```
Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:
```toml
[build]
target = "x86_64-unknown-linux-gnu" # or whatever
```
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`s functionality, but typically optimizes for size over speed.
## Things to Avoid

View File

@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! { cx,
view! {
<progress
max="50"
value=count

View File

@@ -1,5 +0,0 @@
[tasks.web-test]
dependencies = ["cargo-leptos-e2e"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -1,6 +1,3 @@
[env]
END2END_DIR = "end2end"
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
@@ -8,9 +5,6 @@ env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
args = ["fmt", "--", "--check", "--config-path", "../../"]
[tasks.verify-local]
description = "Run all quality checks and tests from an example directory"
dependencies = ["check-style", "test-local"]
@@ -19,62 +13,11 @@ dependencies = ["check-style", "test-local"]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]
[tasks.clean-cargo]
description = "Runs the cargo clean command."
category = "Cleanup"
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
description = "Delete all node_modules directories"
category = "Cleanup"
script = '''
find . -type d -name node_modules | xargs rm -rf
'''
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
cwd = "${END2END_DIR}"
command = "rm"
args = ["-rf", "playwright", "playwright/.cache", "test-results"]
[tasks.clean-all]
description = "Delete all temporary directories"
category = "Cleanup"
dependencies = ["clean-cargo"]
[tasks.wasm-web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.cargo-leptos-e2e]
description = "Runs end to end tests with cargo leptos"
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.setup]
description = "Setup e2e dependencies"
cwd = "${END2END_DIR}"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
if command -v pnpm; then
pnpm install
elif command -v npm; then
npm install
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
'''
dependencies = ["clean", "clean-trunk"]

View File

@@ -1,5 +1,4 @@
[tasks.web-test]
dependencies = ["wasm-web-test"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-trunk"]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -10,12 +10,12 @@ crate-type = ["cdylib", "rlib"]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4.17"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -14,7 +14,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use leptos::{LeptosOptions, view};
use crate::landing::App;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View File

@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{State, Path},
extract::{Extension, Path},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
State(options): State<Arc<LeptosOptions>>,
Extension(options): Extension<Arc<LeptosOptions>>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
@@ -44,7 +44,7 @@ async fn main() {
// 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 = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@@ -58,7 +58,7 @@ async fn main() {
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.with_state(leptos_options);
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -15,7 +15,7 @@ if #[cfg(feature = "ssr")] {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View File

@@ -7,6 +7,7 @@ if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
extract::Extension,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
@@ -17,7 +18,7 @@ if #[cfg(feature = "ssr")] {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@@ -28,7 +29,7 @@ if #[cfg(feature = "ssr")] {
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View File

@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::get,
extract::{Path, State, Extension, RawQuery},
extract::{Path, Extension, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
@@ -33,7 +33,7 @@ if #[cfg(feature = "ssr")] {
}, request).await
}
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, auth_session.clone());
@@ -68,7 +68,7 @@ if #[cfg(feature = "ssr")] {
// 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 = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@@ -80,8 +80,8 @@ if #[cfg(feature = "ssr")] {
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
.with_config(auth_config))
.layer(SessionLayer::new(session_store))
.layer(Extension(pool))
.with_state(leptos_options);
.layer(Extension(Arc::new(leptos_options)))
.layer(Extension(pool));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -14,7 +14,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use leptos::{LeptosOptions, view};
use crate::app::App;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::post, Router};
use axum::{extract::Extension, routing::post, Router};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
@@ -9,7 +9,7 @@ async fn main() {
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@@ -24,7 +24,7 @@ async fn main() {
|cx| view! { cx, <App/> },
)
.fallback(file_and_error_handler)
.with_state(leptos_options);
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -8,10 +8,3 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml

View File

@@ -96,7 +96,6 @@ site-addr = "127.0.0.1:3000"
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
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 tha tool. Controls whether autoreload JS will be included in the head

View File

@@ -1,7 +1,4 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/cargo-leptos-web-test.toml" },
]
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"

View File

@@ -104,8 +104,3 @@ You'll need to install trunk to client side render this bundle.
## Attribution
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/leptos-rs/leptos/discussions/125).
## Playwright Testing
- Run `cargo make setup` to install dependencies
- Run `cargo leptos test` or `cargo leptos end-to-end` to run the tests

View File

@@ -1,7 +1,9 @@
import { test, expect } from "@playwright/test";
test("should see the welcome message", async ({ page }) => {
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
await expect(page).toHaveTitle("Cargo Leptos");
await expect(page.locator("h1")).toHaveText("Hi from your Leptos WASM!");
});

View File

@@ -4,7 +4,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View File

@@ -5,7 +5,7 @@ cfg_if! {
use leptos::*;
use axum::{
routing::{post, get},
extract::{State, Path},
extract::{Extension, Path},
http::Request,
response::{IntoResponse, Response},
Router,
@@ -18,7 +18,7 @@ cfg_if! {
use std::sync::Arc;
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, id.clone());
@@ -42,7 +42,7 @@ cfg_if! {
// 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 = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@@ -52,7 +52,7 @@ cfg_if! {
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -146,8 +146,11 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
</header>
<main>
<Routes>
<Route path="" view=|cx| view! { cx,
<Todos/>
<Route path="" view=|cx| view! {
cx,
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<Todos/>
</ErrorBoundary>
}/> //Route
<Route path="weird" methods=&[Method::Get, Method::Post]
ssr=SsrMode::Async
@@ -200,65 +203,63 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<input type="submit" value="Add"/>
</MultiActionForm>
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
{move || {
let existing_todos = {
move || {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
{move || {
let existing_todos = {
move || {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
.map(move |todo| {
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
.map(move |todo| {
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view(cx)
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
};
view! {
cx,
<ul>
{existing_todos}
{pending_todos}
</ul>
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
};
view! {
cx,
<ul>
{existing_todos}
{pending_todos}
</ul>
}
}
</ErrorBoundary>
}
</Transition>
</div>
}

View File

@@ -43,7 +43,7 @@ impl Todos {
}
pub fn remove(&mut self, id: Uuid) {
self.retain(|todo| todo.id != id);
self.0.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
@@ -76,23 +76,7 @@ impl Todos {
}
fn clear_completed(&mut self) {
self.retain(|todo| !todo.completed.get());
}
fn retain(&mut self, mut f: impl FnMut(&Todo) -> bool) {
self.0.retain(|todo| {
let retain = f(todo);
// because these signals are created at the top level,
// they are owned by the <TodoMVC/> component and not
// by the individual <Todo/> components. This means
// that if they are not manually disposed when removed, they
// will be held onto until the <TodoMVC/> is unmounted.
if !retain {
todo.title.dispose();
todo.completed.dispose();
}
retain
})
self.0.retain(|todo| !todo.completed.get());
}
}
@@ -152,7 +136,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
// Handle the three filter modes: All, Active, and Completed
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener(ev::hashchange, move |_| {
window_event_listener_untyped("hashchange", move |_| {
let new_mode =
location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);

View File

@@ -8,7 +8,6 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Actix integrations for the Leptos web framework."
[dependencies]
actix-http = "3"
actix-web = "4"
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }

View File

@@ -185,7 +185,7 @@ pub fn handle_server_fns_with_context(
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body_ref: &[u8] = &body;
let body: &[u8] = &body;
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
@@ -198,28 +198,10 @@ pub fn handle_server_fns_with_context(
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
// we consume the body here (using the web::Bytes extractor), but it is required for things
// like MultipartForm
if req
.headers()
.get("Content-Type")
.and_then(|value| value.to_str().ok())
.and_then(|value| {
Some(
value.starts_with(
"multipart/form-data; boundary=",
),
)
})
== Some(true)
{
provide_context(cx, body.clone());
}
let query = req.query_string().as_bytes();
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => body_ref,
Encoding::Url | Encoding::Cbor => body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let res = match (server_fn.trait_obj)(cx, data).await {
@@ -1046,7 +1028,7 @@ where
}
}
/// A helper to make it easier to use Actix extractors in server functions. This takes
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
/// will be extracted from the request and returns some value.
@@ -1090,17 +1072,9 @@ where
{
let req = use_context::<actix_web::HttpRequest>(cx)
.expect("HttpRequest should have been provided via context");
let input = if let Some(body) = use_context::<Bytes>(cx) {
let (_, mut payload) = actix_http::h1::Payload::create(false);
payload.unread_data(body);
E::from_request(&req, &mut dev::Payload::from(payload))
} else {
E::extract(&req)
}
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
let input = E::extract(&req)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(f.call(input).await)
}

View File

@@ -1126,39 +1126,12 @@ where
}
}
/// This trait allows one to use your custom struct in Axum's router, provided it can provide the
/// `LeptosOptions` to use for the `LeptosRoutes` trait functions.
pub trait LeptosOptionProvider {
fn options(&self) -> LeptosOptions;
}
/// Implement `LeptosOptionProvider` trait for `LeptosOptions` itself.
impl LeptosOptionProvider for LeptosOptions {
fn options(&self) -> LeptosOptions {
self.clone()
}
}
/// Implement `LeptosOptionProvider` trait for any type wrapped in an Arc, if that type implements
/// `LeptosOptionProvider` as states in axum are often provided wrapped in an Arc.
impl<T> LeptosOptionProvider for Arc<T>
where
T: LeptosOptionProvider,
{
fn options(&self) -> LeptosOptions {
(**self).options()
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes<OP>
where
OP: LeptosOptionProvider,
{
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: OP,
options: LeptosOptions,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1167,7 +1140,7 @@ where
fn leptos_routes_with_context<IV>(
self,
options: OP,
options: LeptosOptions,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -1181,20 +1154,16 @@ where
handler: H,
) -> Self
where
H: axum::handler::Handler<T, OP, axum::body::Body>,
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl<OP> LeptosRoutes<OP> for axum::Router<OP>
where
OP: LeptosOptionProvider + Clone + Send + Sync + 'static,
{
impl LeptosRoutes for axum::Router {
#[tracing::instrument(level = "info", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: OP,
options: LeptosOptions,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1207,7 +1176,7 @@ where
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: OP,
options: LeptosOptions,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -1225,7 +1194,7 @@ where
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.options(),
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
@@ -1239,7 +1208,7 @@ where
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
options.options(),
options.clone(),
additional_context.clone(),
app_fn.clone(),
true
@@ -1254,7 +1223,7 @@ where
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.options(),
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
@@ -1268,7 +1237,7 @@ where
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.options(),
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
@@ -1294,7 +1263,7 @@ where
handler: H,
) -> Self
where
H: axum::handler::Handler<T, OP, axum::body::Body>,
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static,
{
let mut router = self;
@@ -1317,7 +1286,6 @@ where
router
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn get_leptos_pool() -> LocalPoolHandle {
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();

View File

@@ -12,10 +12,6 @@
//!
//! And you can do all three of these **using the same Leptos code.**
//!
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
//! Explore our [Examples](https://github.com/leptos-rs/leptos/tree/main/examples) to see Leptos in action.
//!
//! # `nightly` Note
//! Most of the examples assume youre using `nightly` Rust. If youre on stable, note the following:
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`

View File

@@ -101,17 +101,13 @@ where
use leptos_reactive::signal_prelude::*;
// run the child; we'll probably throw this away, but it will register resource reads
let _child = orig_child(cx).into_view(cx);
let child = orig_child(cx).into_view(cx);
let after_original_child = HydrationCtx::id();
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
let orig_child = Rc::clone(&orig_child);
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
})).into_view(cx)
child
}
// show the fallback, but also prepare to stream HTML
else {

View File

@@ -17,7 +17,7 @@ leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router", default-features = false }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2.85"
wasm-bindgen = "0.2"
serde = "1.0.159"
tokio = { version = "1.27.0", features = ["time"], optional = true }

View File

@@ -56,7 +56,6 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
// in-order
<Route
@@ -72,7 +71,6 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
// async
<Route
@@ -88,7 +86,6 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
<Route path="none" view=|cx| view! { cx, <None/> }/>
</Route>
</Routes>
</main>
@@ -104,7 +101,6 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
<A href="single">"Single"</A>
<A href="parallel">"Parallel"</A>
<A href="inside-component">"Inside Component"</A>
<A href="none">"No Resources"</A>
</nav>
}
}
@@ -221,25 +217,3 @@ fn InsideComponentChild(cx: Scope) -> impl IntoView {
</Suspense>
}
}
#[component]
fn None(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
<div>"Children inside Suspense should hydrate properly."</div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</Suspense>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}

View File

@@ -29,7 +29,7 @@ pub trait EventDescriptor: Clone {
}
}
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
/// Overrides the [`EventDescriptor::bubbles`] method to always return
/// `false`, which forces the event to not be globally delegated.
#[derive(Clone)]
#[allow(non_camel_case_types)]

View File

@@ -807,7 +807,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Sets a style on an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `style`
/// attribute if you use `.attr("style", /* */)`. In the `view` macro, they
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
/// are automatically re-ordered so that this over-writing does not happen.
#[track_caller]
pub fn style(

View File

@@ -240,20 +240,13 @@ impl View {
dont_escape_text: bool,
) {
match self {
View::Suspense(id, view) => {
View::Suspense(id, _) => {
let id = id.to_string();
if let Some(data) = cx.take_pending_fragment(&id) {
chunks.push_back(StreamChunk::Async {
chunks: data.in_order,
should_block: data.should_block,
});
} else {
// if not registered, means it was already resolved
View::CoreComponent(view).into_stream_chunks_helper(
cx,
chunks,
dont_escape_text,
);
}
}
View::Text(node) => {

View File

@@ -672,7 +672,7 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// Annotates a struct so that it can be used with your Component as a `slot`.
///
/// The `#[slot]` macro allows you to annotate plain Rust struct as component slots and use them
/// within your Leptos [`component`](macro@crate::component) properties. The struct can contain any number
/// within your Leptos [component](crate::component!) properties. The struct can contain any number
/// of fields. When you use the component somewhere else, the names of the slot fields are the
/// names of the properties you use in the [view](crate::view!) macro.
///

View File

@@ -42,7 +42,7 @@ web-sys = { version = "0.3", optional = true, features = [
] }
cfg-if = "1"
indexmap = "1"
ouroboros = { version = "0.15.6", default-features = false }
self_cell = "1.0.0"
[dev-dependencies]
criterion = { version = "0.4.0", features = ["html_reports"] }

View File

@@ -188,7 +188,6 @@ impl Runtime {
}
}
#[allow(clippy::await_holding_refcell_ref)] // not using this part of ouroboros
pub(crate) fn mark_dirty(&self, node: NodeId) {
//crate::macros::debug_warn!("marking {node:?} dirty");
let mut nodes = self.nodes.borrow_mut();
@@ -217,23 +216,24 @@ impl Runtime {
* `Check` or `DirtyMarked`.
*
* Because `RefCell`, borrowing the iterators all at once is difficult,
* so a self-referential struct is used instead. ouroboros produces safe
* so a self-referential struct is used instead. self_cell produces safe
* code, but it would not be recommended to use this outside of this
* algorithm.
*/
#[ouroboros::self_referencing]
struct RefIter<'a> {
set: std::cell::Ref<'a, FxIndexSet<NodeId>>,
type Dependent<'a> = indexmap::set::Iter<'a, NodeId>;
// Boxes the iterator internally
#[borrows(set)]
#[covariant]
iter: indexmap::set::Iter<'this, NodeId>,
self_cell::self_cell! {
struct RefIter<'a> {
owner: std::cell::Ref<'a, FxIndexSet<NodeId>>,
#[not_covariant] // avoids extra codegen, harmless to mark it as such
dependent: Dependent,
}
}
/// Due to the limitations of ouroboros, we cannot borrow the
/// stack and iter simultaneously, or directly within the loop,
/// Due to the limitations of self-referencing, we cannot borrow the
/// stack and iter simultaneously within the closure or the loop,
/// therefore this must be used to command the outside scope
/// of what to do.
enum IterResult<'a> {
@@ -251,7 +251,7 @@ impl Runtime {
}
while let Some(iter) = stack.last_mut() {
let res = iter.with_iter_mut(|iter| {
let res = iter.with_dependent_mut(|_, iter| {
let Some(mut child) = iter.next().copied() else {
return IterResult::Empty;
};

View File

@@ -83,7 +83,7 @@ where
)
}
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// read-only half of [`create_slice`].
pub fn create_read_slice<T, O>(
cx: Scope,

View File

@@ -39,7 +39,8 @@ impl<T> Clone for StoredValue<T> {
impl<T> Copy for StoredValue<T> {}
impl<T> StoredValue<T> {
/// Returns a clone of the current stored value.
/// Returns a clone of the signals current value, subscribing the effect
/// to this signal.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
@@ -69,7 +70,7 @@ impl<T> StoredValue<T> {
self.try_get_value().expect("could not get stored value")
}
/// Same as [`StoredValue::get_value`] but will not panic by default.
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
pub fn try_get_value(&self) -> Option<T>
where
@@ -78,7 +79,7 @@ impl<T> StoredValue<T> {
self.try_with_value(T::clone)
}
/// Applies a function to the current stored value and returns the result.
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
@@ -104,8 +105,8 @@ impl<T> StoredValue<T> {
self.try_with_value(f).expect("could not get stored value")
}
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
/// the stored value has not yet been disposed. [`None`] otherwise.
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
with_runtime(self.runtime, |runtime| {
let value = {
@@ -160,8 +161,8 @@ impl<T> StoredValue<T> {
.expect("could not set stored value");
}
/// Same as [`Self::update_value`], but returns [`Some(O)`] if the
/// stored value has not yet been disposed, [`None`] otherwise.
/// Same as [`Self::update`], but returns [`Some(O)`] if the
/// signal is still valid, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
with_runtime(self.runtime, |runtime| {
let values = runtime.stored_values.borrow();
@@ -194,8 +195,8 @@ impl<T> StoredValue<T> {
self.try_set_value(value);
}
/// Same as [`Self::set_value`], but returns [`None`] if the
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
/// Same as [`Self::set`], but returns [`None`] if the signal is
/// still valid, [`Some(T)`] otherwise.
pub fn try_set_value(&self, value: T) -> Option<T> {
with_runtime(self.runtime, |runtime| {
let values = runtime.stored_values.borrow();

View File

@@ -11,7 +11,7 @@ use crate::{
/// Reactive Trigger, notifies reactive code to rerun.
///
/// See [`create_trigger`] for more.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy)]
pub struct Trigger {
pub(crate) runtime: RuntimeId,
pub(crate) id: NodeId,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -95,63 +95,8 @@ where
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
// multipart POST (setting Context-Type breaks the request)
if method == "post" && enctype == "multipart/form-data" {
ev.prevent_default();
ev.stop_propagation();
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.redirect(RequestRedirect::Follow)
.body(form_data)
.send()
.await;
match res {
Err(e) => {
error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
}
// Check all the logical 3xx responses that might
// get returned from a server function
if resp.redirected() {
let resp_url = &resp.url();
match Url::try_from(resp_url.as_str()) {
Ok(url) => {
request_animation_frame(move || {
if let Err(e) = navigate(
&format!(
"{}{}",
url.pathname, url.search,
),
Default::default(),
) {
warn!("{}", e);
}
});
}
Err(e) => warn!("{}", e),
}
}
}
}
});
}
// POST
else if method == "post" {
if method == "post" {
ev.prevent_default();
ev.stop_propagation();