mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 14:52:35 -05:00
Compare commits
2 Commits
server_fn_
...
PenguinWit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa73ff4db | ||
|
|
7291efc077 |
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -7,6 +7,7 @@ updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "/"
|
||||
- "/examples/*"
|
||||
- "/benchmarks"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
49
.github/workflows/autofix.yml
vendored
49
.github/workflows/autofix.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: autofix.ci
|
||||
on:
|
||||
pull_request:
|
||||
# Running this workflow on main branch pushes requires write permission to apply changes.
|
||||
# Leave it alone for future uses.
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- run: |
|
||||
echo "Formatting the workspace"
|
||||
cargo fmt --all
|
||||
|
||||
echo "Running Clippy against each member's features (default features included)"
|
||||
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
|
||||
echo "Working on member $member":
|
||||
echo -e "\tdefault-features/no-features:"
|
||||
# this will also run on members with no features or default features
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member"
|
||||
|
||||
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
|
||||
for feature in $features; do
|
||||
if [ "$feature" = "default" ]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\tfeature $feature"
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
|
||||
done
|
||||
done
|
||||
- uses: autofix-ci/action@v1.3.1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
fail-fast: false
|
||||
13
.github/workflows/run-cargo-make-task.yml
vendored
13
.github/workflows/run-cargo-make-task.yml
vendored
@@ -19,19 +19,6 @@ jobs:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
run: |
|
||||
echo "Disk space before cleanup:"
|
||||
df -h
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo rm -rf /usr/local/lib/android/sdk/ndk
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
sudo apt-get clean
|
||||
echo "Disk space after cleanup:"
|
||||
df -h
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Rust
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,9 +3,7 @@ dist
|
||||
pkg
|
||||
comparisons
|
||||
blob.rs
|
||||
**/projects/**/Cargo.lock
|
||||
**/examples/**/Cargo.lock
|
||||
**/benchmarks/**/Cargo.lock
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -13,5 +11,4 @@ blob.rs
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
vendor
|
||||
hash.txt
|
||||
vendor
|
||||
4474
Cargo.lock
generated
4474
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
42
Cargo.toml
42
Cargo.toml
@@ -40,36 +40,36 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-rc1"
|
||||
version = "0.7.0-gamma3"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-rc1" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-rc1" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-rc1" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-rc1" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-rc1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-rc1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-rc1" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-rc1" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-rc1" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-rc1" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-rc1" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-rc1" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-rc1" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-rc1" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-rc1" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-rc1" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-rc1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-rc1" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-rc1" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-rc1" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -159,7 +159,9 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
|
||||
- Use event listeners to update signals
|
||||
- Create effects to update the UI
|
||||
|
||||
The 0.7 update originally set out to create a "generic rendering" approach that would allow us to reuse most of the same view logic to do all of the above. Unfortunately, this has had to be shelved for now due to difficulties encountered by the Rust compiler when building larger-scale applications with the number of generics spread throughout the codebase that this required. It's an approach I'm looking forward to exploring again in the future; feel free to reach out if you're interested in this kind of work.
|
||||
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
|
||||
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
|
||||
|
||||
### How is this different from Yew?
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.15"
|
||||
pin-project-lite = "0.2.14"
|
||||
|
||||
@@ -10,14 +10,14 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
futures = "0.3.30"
|
||||
glib = { version = "0.20.0", optional = true }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.45", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
|
||||
@@ -291,10 +291,9 @@ impl Executor {
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_custom_executor(
|
||||
custom_executor: impl CustomExecutor + Send + Sync + 'static,
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor + Send + Sync>> =
|
||||
OnceLock::new();
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
EXECUTOR
|
||||
.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
@@ -312,46 +311,13 @@ impl Executor {
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Locally sets a custom executor as the executor used to spawn tasks
|
||||
/// in the current thread.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_local_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
thread_local! {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
}
|
||||
EXECUTOR.with(|this| {
|
||||
this.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)
|
||||
})?;
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().spawn_local(fut));
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
EXECUTOR.with(|this| this.get().unwrap().poll_local());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for custom executors.
|
||||
/// Custom executors can be used to integrate with any executor that supports spawning futures.
|
||||
///
|
||||
/// All methods can be called recursively.
|
||||
pub trait CustomExecutor {
|
||||
pub trait CustomExecutor: Send + Sync {
|
||||
/// Spawns a future, usually on a thread pool.
|
||||
fn spawn(&self, fut: PinnedFuture<()>);
|
||||
/// Spawns a local future. May require calling `poll_local` to make progress.
|
||||
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.15"
|
||||
pin-project-lite = "0.2.14"
|
||||
|
||||
@@ -26,7 +26,6 @@ async fn main() {
|
||||
};
|
||||
use axum_js_ssr::app::*;
|
||||
use http_body_util::BodyExt;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter::*;
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.service(Files::new("/", site_root))
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use counter_without_macros::counter;
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use directives::App;
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
@@ -6,7 +6,9 @@ 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.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(#[prop(into)] errors: Signal<Errors>) -> impl IntoView {
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(into)] errors: MaybeSignal<Errors>,
|
||||
) -> impl IntoView {
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors = Memo::new(move |_| {
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -28,7 +28,9 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -56,7 +56,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -21,16 +21,10 @@ pub fn Nav() -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a
|
||||
class="github"
|
||||
href="http://github.com/leptos-rs/leptos"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -50,42 +50,30 @@ pub fn Stories() -> impl IntoView {
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
{move || {
|
||||
if page() > 1 {
|
||||
Either::Left(
|
||||
view! {
|
||||
<a
|
||||
class="page-link"
|
||||
href=move || {
|
||||
format!("/{}?page={}", story_type(), page() - 1)
|
||||
}
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(
|
||||
view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
},
|
||||
)
|
||||
}
|
||||
{move || if page() > 1 {
|
||||
Either::Left(view! {
|
||||
<a class="page-link"
|
||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
})
|
||||
} else {
|
||||
Either::Right(view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
})
|
||||
}}
|
||||
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Suspense>
|
||||
<span
|
||||
class="page-link"
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<a
|
||||
href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
@@ -95,10 +83,14 @@ pub fn Stories() -> impl IntoView {
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Transition fallback=move || view! { <p>"Loading..."</p> } set_pending>
|
||||
<Show when=move || {
|
||||
stories.read().as_ref().map(Option::is_none).unwrap_or(false)
|
||||
}>> <p>"Error loading stories."</p></Show>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
set_pending
|
||||
>
|
||||
<Show when=move || stories.read().as_ref().map(Option::is_none).unwrap_or(false)>
|
||||
>
|
||||
<p>"Error loading stories."</p>
|
||||
</Show>
|
||||
<ul>
|
||||
<For
|
||||
each=move || stories.get().unwrap_or_default().unwrap_or_default()
|
||||
@@ -113,78 +105,54 @@ pub fn Stories() -> impl IntoView {
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Story(story: api::Story) -> impl IntoView {
|
||||
view! {
|
||||
<li class="news-item">
|
||||
<li class="news-item">
|
||||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
</span>
|
||||
},
|
||||
)
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
})
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
Either::Right(view! { <A href=format!("/stories/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
|
||||
</span>
|
||||
<br/>
|
||||
<br />
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!(
|
||||
"{} comments",
|
||||
story.comments_count.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
|
||||
</A>
|
||||
</span>
|
||||
},
|
||||
)
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
</A>
|
||||
</span>
|
||||
})
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
Either::Right(view! { <A href=format!("/item/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
|
||||
</span>
|
||||
{(story.story_type != "link")
|
||||
.then(|| {
|
||||
view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
}
|
||||
})}
|
||||
|
||||
{(story.story_type != "link").then(|| view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -28,21 +28,18 @@ pub fn Story() -> impl IntoView {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
@@ -51,7 +48,6 @@ pub fn Story() -> impl IntoView {
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
@@ -59,7 +55,7 @@ pub fn Story() -> impl IntoView {
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment/>
|
||||
<Comment comment />
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -68,7 +64,6 @@ pub fn Story() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
}))).build())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -77,65 +72,43 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<li class="comment">
|
||||
<div class="by">
|
||||
<A href=format!(
|
||||
"/users/{}",
|
||||
comment.user.clone().unwrap_or_default(),
|
||||
)>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty())
|
||||
.then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| {
|
||||
set_open.update(|n| *n = !*n)
|
||||
}>
|
||||
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || {
|
||||
if open.get() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!(
|
||||
"[+] {}{} collapsed",
|
||||
comments_len,
|
||||
pluralize(comments_len),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{move || {
|
||||
open
|
||||
.get()
|
||||
.then({
|
||||
let comments = comment.comments.clone();
|
||||
move || {
|
||||
view! {
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment/>
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
<div class="by">
|
||||
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty()).then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || if open.get() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
|
||||
}
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
{move || open.get().then({
|
||||
let comments = comment.comments.clone();
|
||||
move || view! {
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</li>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
@@ -18,48 +18,30 @@ pub fn User() -> impl IntoView {
|
||||
);
|
||||
view! {
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || Suspend::new(async move {
|
||||
match user.await.clone() {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span>
|
||||
{user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span>
|
||||
{user.karma}
|
||||
</li>
|
||||
<li inner_html=user.about class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!(
|
||||
"https://news.ycombinator.com/submitted?id={}",
|
||||
user.id,
|
||||
)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!(
|
||||
"https://news.ycombinator.com/threads?id={}",
|
||||
user.id,
|
||||
)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})}
|
||||
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || Suspend::new(async move { match user.await.clone() {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => Either::Right(view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
<li inner_html={user.about} class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
})
|
||||
}})}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,7 +46,9 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -12,7 +12,7 @@ lto = true
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos", features = ["islands"] }
|
||||
leptos = { path = "../../leptos", features = ["experimental-islands"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -42,7 +42,9 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,7 +46,9 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -12,7 +12,7 @@ futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
"experimental-islands",
|
||||
] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
|
||||
@@ -12,7 +12,7 @@ futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
|
||||
@@ -44,8 +44,8 @@ window.addEventListener("click", async (ev) => {
|
||||
// TODO parse from the request stream instead?
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = document.startViewTransition(async () => {
|
||||
const oldDocWalker = document.createTreeWalker(document);
|
||||
const newDocWalker = doc.createTreeWalker(doc);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
@@ -128,13 +128,8 @@ window.addEventListener("click", async (ev) => {
|
||||
}
|
||||
} }
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
});
|
||||
await transition;
|
||||
window.history.pushState(undefined, null, url);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
let handle = mount_to(
|
||||
document()
|
||||
helpers::document()
|
||||
.get_element_by_id("app")
|
||||
.unwrap()
|
||||
.unchecked_into(),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use portal::App;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<link data-trunk rel="css" href="style.css"/>
|
||||
<link data-trunk rel="css" href="style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
}>{move || if logged_in.get() { "Log Out" } else { "Log In" }}</button>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes transition=true fallback=|| "This page could not be found.">
|
||||
<Routes fallback=|| "This page could not be found.">
|
||||
// paths can be created using the path!() macro, or provided as types like
|
||||
// StaticSegment("about")
|
||||
<Route path=path!("about") view=About/>
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
.routing-progress {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
a[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -17,8 +12,12 @@ a[aria-current] {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.contact {
|
||||
view-transition-name: contact;
|
||||
.fadeIn {
|
||||
animation: 0.5s fadeIn forwards;
|
||||
}
|
||||
|
||||
.fadeOut {
|
||||
animation: 0.5s fadeOut forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -41,44 +40,12 @@ a[aria-current] {
|
||||
}
|
||||
}
|
||||
|
||||
.router-outlet-0 main {
|
||||
view-transition-name: main;
|
||||
.slideIn {
|
||||
animation: 0.25s slideIn forwards;
|
||||
}
|
||||
|
||||
.router-back main {
|
||||
view-transition-name: main-back;
|
||||
}
|
||||
|
||||
.router-outlet-1 .contact-list {
|
||||
view-transition-name: contact;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
::view-transition-old(contact) {
|
||||
animation: 0.5s fadeOut;
|
||||
}
|
||||
|
||||
::view-transition-new(contact) {
|
||||
animation: 0.5s fadeIn;
|
||||
}
|
||||
|
||||
::view-transition-old(main) {
|
||||
animation: 0.5s slideOut;
|
||||
}
|
||||
|
||||
::view-transition-new(main) {
|
||||
animation: 0.5s slideIn;
|
||||
}
|
||||
|
||||
::view-transition-old(main-back) {
|
||||
color: red;
|
||||
animation: 0.5s slideOutBack;
|
||||
}
|
||||
|
||||
::view-transition-new(main-back) {
|
||||
color: blue;
|
||||
animation: 0.5s slideInBack;
|
||||
}
|
||||
.slideOut {
|
||||
animation: 0.25s slideOut forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@@ -99,6 +66,14 @@ a[aria-current] {
|
||||
}
|
||||
}
|
||||
|
||||
.slideInBack {
|
||||
animation: 0.25s slideInBack forwards;
|
||||
}
|
||||
|
||||
.slideOutBack {
|
||||
animation: 0.25s slideOutBack forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInBack {
|
||||
from {
|
||||
transform: translate(-100vw, 0);
|
||||
|
||||
@@ -10,7 +10,7 @@ struct Then {
|
||||
// the type with Option<...> and marking the option as #[prop(optional)].
|
||||
#[slot]
|
||||
struct ElseIf {
|
||||
cond: Signal<bool>,
|
||||
cond: MaybeSignal<bool>,
|
||||
children: ChildrenFn,
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct Fallback {
|
||||
// Slots are added to components like any other prop.
|
||||
#[component]
|
||||
fn SlotIf(
|
||||
cond: Signal<bool>,
|
||||
cond: MaybeSignal<bool>,
|
||||
then: Then,
|
||||
#[prop(default=vec![])] else_if: Vec<ElseIf>,
|
||||
#[prop(optional)] fallback: Option<Fallback>,
|
||||
@@ -43,9 +43,9 @@ fn SlotIf(
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (count, set_count) = signal(0);
|
||||
let is_even = Signal::derive(move || count.get() % 2 == 0);
|
||||
let is_div5 = Signal::derive(move || count.get() % 5 == 0);
|
||||
let is_div7 = Signal::derive(move || count.get() % 7 == 0);
|
||||
let is_even = MaybeSignal::derive(move || count.get() % 2 == 0);
|
||||
let is_div5 = MaybeSignal::derive(move || count.get() % 5 == 0);
|
||||
let is_div7 = MaybeSignal::derive(move || count.get() % 7 == 0);
|
||||
|
||||
view! {
|
||||
<button on:click=move |_| set_count.update(|value| *value += 1)>"+1"</button>
|
||||
|
||||
@@ -39,7 +39,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use ssr_modes_axum::app::*;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list_with_ssg, LeptosRoutes};
|
||||
use static_routing::app::*;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::logging::warn;
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Patch, Store};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -110,7 +109,11 @@ pub fn App() -> impl IntoView {
|
||||
// directly implements IntoIterator, so we can use it in <For/> and
|
||||
// it will manage reactivity for the store fields correctly
|
||||
<For
|
||||
each=move || store.todos()
|
||||
each=move || {
|
||||
leptos::logging::log!("RERUNNING FOR CALCULATION");
|
||||
store.todos()
|
||||
}
|
||||
|
||||
key=|row| row.id().get()
|
||||
let:todo
|
||||
>
|
||||
|
||||
@@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6.6", optional = true }
|
||||
actix-web = { version = "4.8", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
js-sys = { version = "0.3.72" }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
@@ -20,10 +19,7 @@ serde = "1.0"
|
||||
tokio = { version = "1.39", features = ["time", "rt"], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
|
||||
"leptos/hydrate",
|
||||
]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
@check_aria_current
|
||||
Feature: Check aria-current being applied to make links bolded
|
||||
|
||||
Background:
|
||||
|
||||
Given I see the app
|
||||
|
||||
Scenario: Should see the base case working
|
||||
Then I see the link Out-of-Order being bolded
|
||||
Then I see the following links being bolded
|
||||
| Out-of-Order |
|
||||
| Nested |
|
||||
@@ -1,94 +0,0 @@
|
||||
@check_instrumented
|
||||
Feature: Instrumented Counters showing the expected values
|
||||
|
||||
Scenario: I can fresh CSR instrumented counters
|
||||
Given I see the app
|
||||
When I access the instrumented counters via CSR
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: I should see counter going up after viewing Item Listing
|
||||
Given I see the app
|
||||
When I select the following links
|
||||
| Instrumented |
|
||||
| Item Listing |
|
||||
| Counters |
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 1 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
# the reload has happened in Item Listing, it follows a suspend
|
||||
# will be called as hydration happens.
|
||||
Scenario: Refreshing Item Listing should have only suspend counters
|
||||
Given I see the app
|
||||
When I access the instrumented counters via SSR
|
||||
And I select the component Item Listing
|
||||
And I reload the page
|
||||
And I select the component Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Reset CSR Counters work as expected.
|
||||
Given I see the app
|
||||
When I access the instrumented counters via SSR
|
||||
And I select the component Item Listing
|
||||
And I click on Reset CSR Counters
|
||||
And I select the component Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 0 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Standard usage of the instruments traversing down
|
||||
Given I see the app
|
||||
When I select the following links
|
||||
| Instrumented |
|
||||
| Item Listing |
|
||||
| Item 2 |
|
||||
| Inspect path3 |
|
||||
| Inspect path3/field1 |
|
||||
And I access the instrumented counters via CSR
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 1 |
|
||||
| item_inspect | 2 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 1 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 1 |
|
||||
@@ -1,187 +0,0 @@
|
||||
@check_instrumented_suspense_resource
|
||||
Feature: Using instrumented counters for real
|
||||
Check that the suspend/suspense and the underlying resources are
|
||||
called with the expected number of times for CSR rendering.
|
||||
|
||||
Background:
|
||||
|
||||
Given I see the app
|
||||
And I select the mode Instrumented
|
||||
|
||||
Scenario: Emulate steps 1 to 5 of issue #2961
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 0 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate step 6 of issue #2961
|
||||
Given I select the link Target 41#
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 42# |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 1 |
|
||||
| item_inspect | 2 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate step 7 of issue #2961
|
||||
Given I select the link Target 42#
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Target 4## |
|
||||
| Target 42# |
|
||||
| Target 41# |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 0 |
|
||||
| item_overview | 1 |
|
||||
| item_inspect | 3 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 2 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate step 8, "not trigger double fetch".
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
| Target 41# |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 2 |
|
||||
| item_inspect | 1 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Like above, for the "double fetch" which shouldn't happen
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
| Target 41# |
|
||||
| Target 3## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 3 |
|
||||
| item_inspect | 1 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 2 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Like above, but using 4## instead
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the following links
|
||||
| Item Listing |
|
||||
| Target 4## |
|
||||
| Target 41# |
|
||||
| Target 4## |
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Suspend Calls | |
|
||||
| item_listing | 1 |
|
||||
| item_overview | 3 |
|
||||
| item_inspect | 1 |
|
||||
And the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 1 |
|
||||
| inspect_item_root | 1 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
# The following tests previously showed the clear difference between
|
||||
# hydration and CSR, where hydration resulting in extra server API
|
||||
# calls via the resource while CSR did not suffer from the issue.
|
||||
# With #3182 merged the issue is corrected, going up to components
|
||||
# specified by the parent route should no longer result in the
|
||||
# superfluous fetches for resources needed by component about to be
|
||||
# unmounted.
|
||||
Scenario: Emulate part of step 8 of issue #2961
|
||||
Given I select the link Target 3##
|
||||
And I refresh the page
|
||||
When I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate above, instead of refresh page, reset csr counters
|
||||
Given I select the link Target 3##
|
||||
And I click on Reset CSR Counters
|
||||
When I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
# Further two sets for good measure.
|
||||
Scenario: Start with hydration from Target 41# and go up
|
||||
Given I select the link Target 41#
|
||||
And I refresh the page
|
||||
When I select the link Target 4##
|
||||
And I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
|
||||
Scenario: Emulate the same csr counter reset, for Target 41#.
|
||||
Given I select the link Target 41#
|
||||
And I click on Reset CSR Counters
|
||||
When I select the link Target 4##
|
||||
And I select the link Item Listing
|
||||
And I go check the Counters
|
||||
Then I see the following counters under section
|
||||
| Server Calls (CSR) | |
|
||||
| list_items | 0 |
|
||||
| get_item | 0 |
|
||||
| inspect_item_root | 0 |
|
||||
| inspect_item_field | 0 |
|
||||
@@ -37,19 +37,3 @@ pub async fn click_second_button(client: &Client) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_reset_counters_button(client: &Client) -> Result<()> {
|
||||
let reset_counter = find::reset_counter(client).await?;
|
||||
|
||||
reset_counter.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_reset_csr_counters_button(client: &Client) -> Result<()> {
|
||||
let reset_counter = find::reset_csr_counter(client).await?;
|
||||
|
||||
reset_counter.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -63,30 +63,3 @@ pub async fn second_count_is(client: &Client, expected: u32) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn instrumented_counts(
|
||||
client: &Client,
|
||||
expected: &[(&str, u32)],
|
||||
) -> Result<()> {
|
||||
let mut actual = Vec::<(&str, u32)>::new();
|
||||
|
||||
for (selector, _) in expected.iter() {
|
||||
actual.push((
|
||||
selector,
|
||||
find::instrumented_count(client, selector).await?,
|
||||
))
|
||||
}
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn link_text_is_aria_current(client: &Client, text: &str) -> Result<()> {
|
||||
let link = find::link_with_text(client, text).await?;
|
||||
|
||||
link.attr("aria-current").await?
|
||||
.expect(format!("aria-current missing for {text}").as_str());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -77,43 +77,6 @@ pub async fn second_button(client: &Client) -> Result<Element> {
|
||||
Ok(counter_button)
|
||||
}
|
||||
|
||||
pub async fn instrumented_count(
|
||||
client: &Client,
|
||||
selector: &str,
|
||||
) -> Result<u32> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Id(selector))
|
||||
.await
|
||||
.expect(format!("Element #{selector} not found.")
|
||||
.as_str());
|
||||
let text = element.text().await?;
|
||||
let count = text.parse::<u32>()
|
||||
.expect(format!("Element #{selector} does not contain a number.")
|
||||
.as_str());
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn reset_counter(client: &Client) -> Result<Element> {
|
||||
let reset_button = client
|
||||
.wait()
|
||||
.for_element(Locator::Id("reset-counters"))
|
||||
.await
|
||||
.expect("Reset counter input not found");
|
||||
|
||||
Ok(reset_button)
|
||||
}
|
||||
|
||||
pub async fn reset_csr_counter(client: &Client) -> Result<Element> {
|
||||
let reset_button = client
|
||||
.wait()
|
||||
.for_element(Locator::Id("reset-csr-counters"))
|
||||
.await
|
||||
.expect("Reset CSR counter input not found");
|
||||
|
||||
Ok(reset_button)
|
||||
}
|
||||
|
||||
async fn component_message(client: &Client, id: &str) -> Result<String> {
|
||||
let element =
|
||||
client.wait().for_element(Locator::Id(id)).await.expect(
|
||||
@@ -124,12 +87,3 @@ async fn component_message(client: &Client, id: &str) -> Result<String> {
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
|
||||
let link = client
|
||||
.wait()
|
||||
.for_element(Locator::LinkText(text))
|
||||
.await
|
||||
.expect(format!("Link not found by `{}`", text).as_str());
|
||||
Ok(link)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{given, when, gherkin::Step};
|
||||
use cucumber::{given, when};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
@@ -12,13 +12,19 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
}
|
||||
|
||||
#[given(regex = r"^I select the mode (.*)$")]
|
||||
async fn i_select_the_mode(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = r"^I select the component (.*)$")]
|
||||
#[when(regex = "^I select the component (.*)$")]
|
||||
#[given(regex = "^I select the link (.*)$")]
|
||||
#[when(regex = "^I select the link (.*)$")]
|
||||
#[when(regex = "^I click on the link (.*)$")]
|
||||
#[when(regex = "^I go check the (.*)$")]
|
||||
async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
async fn i_select_the_component(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, &text).await?;
|
||||
|
||||
@@ -53,69 +59,3 @@ async fn i_click_the_second_button_n_times(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I (refresh|reload) the (browser|page)$")]
|
||||
#[when(regex = "^I (refresh|reload) the (browser|page)$")]
|
||||
async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
client.refresh().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(expr = "I click on Reset Counters")]
|
||||
async fn i_click_on_reset_counters(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_reset_counters_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "I click on Reset CSR Counters")]
|
||||
#[when(expr = "I click on Reset CSR Counters")]
|
||||
async fn i_click_on_reset_csr_counters(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_reset_csr_counters_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(expr = "I access the instrumented counters via SSR")]
|
||||
async fn i_access_the_instrumented_counters_page_via_ssr(
|
||||
world: &mut AppWorld,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, "Instrumented").await?;
|
||||
action::click_link(client, "Counters").await?;
|
||||
client.refresh().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(expr = "I access the instrumented counters via CSR")]
|
||||
async fn i_access_the_instrumented_counters_page_via_csr(
|
||||
world: &mut AppWorld,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_link(client, "Instrumented").await?;
|
||||
action::click_link(client, "Counters").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(expr = "I select the following links")]
|
||||
#[when(expr = "I select the following links")]
|
||||
async fn i_select_the_following_links(
|
||||
world: &mut AppWorld,
|
||||
step: &Step,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
if let Some(table) = step.table.as_ref() {
|
||||
for row in table.rows.iter() {
|
||||
action::click_link(client, &row[0]).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{then, gherkin::Step};
|
||||
use cucumber::then;
|
||||
|
||||
#[then(regex = r"^I see the page title is (.*)$")]
|
||||
async fn i_see_the_page_title_is(
|
||||
@@ -79,49 +79,3 @@ async fn i_see_the_second_count_is(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = r"^I see the link (.*) being bolded$")]
|
||||
async fn i_see_the_link_being_bolded(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::link_text_is_aria_current(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(expr = "I see the following links being bolded")]
|
||||
async fn i_see_the_following_links_being_bolded(
|
||||
world: &mut AppWorld,
|
||||
step: &Step,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
if let Some(table) = step.table.as_ref() {
|
||||
for row in table.rows.iter() {
|
||||
check::link_text_is_aria_current(client, &row[0]).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(expr = "I see the following counters under section")]
|
||||
#[then(expr = "the following counters under section")]
|
||||
async fn i_see_the_following_counters_under_section(
|
||||
world: &mut AppWorld,
|
||||
step: &Step,
|
||||
) -> Result<()> {
|
||||
// FIXME ideally check the mode; for now leave it because effort
|
||||
let client = &world.client;
|
||||
if let Some(table) = step.table.as_ref() {
|
||||
let expected = table.rows
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|row| (row[0].as_str(), row[1].parse::<u32>().unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
check::instrumented_counts(client, &expected).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::instrumented::InstrumentedRoutes;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{Outlet, ParentRoute, Redirect, Route, Router, Routes, A},
|
||||
@@ -42,7 +41,6 @@ pub fn App() -> impl IntoView {
|
||||
<A href="/out-of-order">"Out-of-Order"</A>
|
||||
<A href="/in-order">"In-Order"</A>
|
||||
<A href="/async">"Async"</A>
|
||||
<A href="/instrumented/">"Instrumented"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.">
|
||||
@@ -112,7 +110,6 @@ pub fn App() -> impl IntoView {
|
||||
<Route path=StaticSegment("local") view=LocalResource/>
|
||||
<Route path=StaticSegment("none") view=None/>
|
||||
</ParentRoute>
|
||||
<InstrumentedRoutes/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -1,667 +0,0 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{ParentRoute, Route, A},
|
||||
hooks::use_params,
|
||||
nested_router::Outlet,
|
||||
params::Params,
|
||||
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(super) mod counter {
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
LazyLock, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(0))
|
||||
}
|
||||
|
||||
pub fn get(&self) -> u32 {
|
||||
self.0.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn inc(&self) -> u32 {
|
||||
self.0.fetch_add(1, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.0.store(0, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Counters {
|
||||
pub list_items: Counter,
|
||||
pub get_item: Counter,
|
||||
pub inspect_item_root: Counter,
|
||||
pub inspect_item_field: Counter,
|
||||
}
|
||||
|
||||
impl From<&mut Counters> for super::Counters {
|
||||
fn from(counter: &mut Counters) -> Self {
|
||||
Self {
|
||||
get_item: counter.get_item.get(),
|
||||
inspect_item_root: counter.inspect_item_root.get(),
|
||||
inspect_item_field: counter.inspect_item_field.get(),
|
||||
list_items: counter.list_items.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Counters {
|
||||
pub fn reset(&self) {
|
||||
self.get_item.reset();
|
||||
self.inspect_item_root.reset();
|
||||
self.inspect_item_field.reset();
|
||||
self.list_items.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub static COUNTERS: LazyLock<Mutex<HashMap<u64, Counters>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct Item {
|
||||
id: i64,
|
||||
name: Option<String>,
|
||||
field: Option<String>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn list_items(ticket: u64) -> Result<Vec<i64>, ServerFnError> {
|
||||
// emulate database query overhead
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.list_items
|
||||
.inc();
|
||||
Ok(vec![1, 2, 3, 4])
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct GetItemResult(pub Item, pub Vec<String>);
|
||||
|
||||
#[server]
|
||||
async fn get_item(
|
||||
ticket: u64,
|
||||
id: i64,
|
||||
) -> Result<GetItemResult, ServerFnError> {
|
||||
// emulate database query overhead
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.get_item
|
||||
.inc();
|
||||
let name = None::<String>;
|
||||
let field = None::<String>;
|
||||
Ok(GetItemResult(
|
||||
Item { id, name, field },
|
||||
["path1", "path2", "path3"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct InspectItemResult(pub Item, pub String, pub Vec<String>);
|
||||
|
||||
#[server]
|
||||
async fn inspect_item(
|
||||
ticket: u64,
|
||||
id: i64,
|
||||
path: String,
|
||||
) -> Result<InspectItemResult, ServerFnError> {
|
||||
// emulate database query overhead
|
||||
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
||||
let mut split = path.split('/');
|
||||
let name = split.next().map(str::to_string);
|
||||
let path = name
|
||||
.clone()
|
||||
.expect("name should have been defined at this point");
|
||||
let field = split
|
||||
.next()
|
||||
.and_then(|s| (!s.is_empty()).then(|| s.to_string()));
|
||||
if field.is_none() {
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.inspect_item_root
|
||||
.inc();
|
||||
} else {
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.inspect_item_field
|
||||
.inc();
|
||||
}
|
||||
Ok(InspectItemResult(
|
||||
Item { id, name, field },
|
||||
path,
|
||||
["field1", "field2", "field3"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct Counters {
|
||||
pub get_item: u32,
|
||||
pub inspect_item_root: u32,
|
||||
pub inspect_item_field: u32,
|
||||
pub list_items: u32,
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn get_counters(ticket: u64) -> Result<Counters, ServerFnError> {
|
||||
Ok((*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.into())
|
||||
}
|
||||
|
||||
#[server(ResetCounters)]
|
||||
async fn reset_counters(ticket: u64) -> Result<(), ServerFnError> {
|
||||
(*counter::COUNTERS)
|
||||
.lock()
|
||||
.expect("somehow panicked elsewhere")
|
||||
.entry(ticket)
|
||||
.or_default()
|
||||
.reset();
|
||||
// leptos::logging::log!("counters for ticket {ticket} have been reset");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SuspenseCounters {
|
||||
item_overview: u32,
|
||||
item_inspect: u32,
|
||||
item_listing: u32,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
// TODO should make this mode configurable via feature flag?
|
||||
let ssr = SsrMode::Async;
|
||||
view! {
|
||||
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop/>
|
||||
<ParentRoute path=StaticSegment("item") view=ItemRoot>
|
||||
<Route path=StaticSegment("/") view=ItemListing/>
|
||||
<ParentRoute path=ParamSegment("id") view=ItemTop>
|
||||
<Route path=StaticSegment("/") view=ItemOverview/>
|
||||
<Route path=WildcardSegment("path") view=ItemInspect/>
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Ticket(pub u64);
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct CSRTicket(pub u64);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn inst_ticket() -> u64 {
|
||||
// SSR will always use 0 for the ticket
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn inst_ticket() -> u64 {
|
||||
// CSR will use a random number for the ticket
|
||||
(js_sys::Math::random() * ((u64::MAX - 1) as f64) + 1f64) as u64
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InstrumentedRoot() -> impl IntoView {
|
||||
let counters = RwSignal::new(SuspenseCounters::default());
|
||||
provide_context(counters);
|
||||
provide_field_nav_portlet_context();
|
||||
|
||||
// Generate a ID directly on this component. Rather than relying on
|
||||
// additional server functions, doing it this way emulates more
|
||||
// standard workflows better and to avoid having to add another
|
||||
// thing to instrument/interfere with the typical use case.
|
||||
// Downside is that randomness has a chance to conflict.
|
||||
//
|
||||
// Furthermore, this approach **will** result in unintuitive
|
||||
// behavior when it isn't accounted for - specifically, the reason
|
||||
// for this design is that when SSR it will guarantee usage of `0`
|
||||
// as the ticket, while CSR it will be of some other value as the
|
||||
// version it uses will be random. However, when trying to get back
|
||||
// the counters associated with the ticket, rendering using SSR will
|
||||
// always produce the SSR version and this quirk will need to be
|
||||
// accounted for.
|
||||
let ticket = inst_ticket();
|
||||
// leptos::logging::log!(
|
||||
// "Ticket for this InstrumentedRoot instance: {ticket}"
|
||||
// );
|
||||
provide_context(Ticket(ticket));
|
||||
|
||||
let csr_ticket = RwSignal::<Option<CSRTicket>>::new(None);
|
||||
|
||||
let reset_counters = ServerAction::<ResetCounters>::new();
|
||||
|
||||
Effect::new(move |_| {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
csr_ticket.set(Some(CSRTicket(ticket)));
|
||||
});
|
||||
|
||||
view! {
|
||||
<section id="instrumented">
|
||||
<nav>
|
||||
<a href="/">"Site Root"</a>
|
||||
<A href="./" exact=true>"Instrumented Root"</A>
|
||||
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
|
||||
<A href="counters" strict_trailing_slash=true>"Counters"</A>
|
||||
</nav>
|
||||
<FieldNavPortlet/>
|
||||
<Outlet/>
|
||||
<Suspense>{
|
||||
move || Suspend::new(async move {
|
||||
let clear_suspense_counters = move |_| {
|
||||
counters.update(|c| *c = SuspenseCounters::default());
|
||||
};
|
||||
csr_ticket.get().map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
}</Suspense>
|
||||
<footer>
|
||||
<nav>
|
||||
<A href="item/3/">"Target 3##"</A>
|
||||
<A href="item/4/">"Target 4##"</A>
|
||||
<A href="item/4/path1/">"Target 41#"</A>
|
||||
<A href="item/4/path2/">"Target 42#"</A>
|
||||
<A href="item/4/path2/field1">"Target 421"</A>
|
||||
<A href="item/1/path2/field3">"Target 123"</A>
|
||||
</nav>
|
||||
</footer>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InstrumentedTop() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Instrumented Tests"</h1>
|
||||
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
|
||||
<ul>
|
||||
// not using `A` because currently some bugs with artix
|
||||
<li><a href="item/">"Item Listing"</a></li>
|
||||
<li><a href="item/4/path1/">"Target 41#"</a></li>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemRoot() -> impl IntoView {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
provide_context(Resource::new_blocking(
|
||||
move || (),
|
||||
move |_| async move { list_items(ticket).await },
|
||||
));
|
||||
|
||||
view! {
|
||||
<h2>"<ItemRoot/>"</h2>
|
||||
<Outlet/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemListing() -> impl IntoView {
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let resource =
|
||||
expect_context::<Resource<Result<Vec<i64>, ServerFnError>>>();
|
||||
let item_listing = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|items| items
|
||||
.into_iter()
|
||||
.map(move |item|
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
|
||||
}
|
||||
)
|
||||
.collect_view()
|
||||
);
|
||||
suspense_counters.update_untracked(|c| c.item_listing += 1);
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h3>"<ItemListing/>"</h3>
|
||||
<ul>
|
||||
<Suspense>
|
||||
{item_listing}
|
||||
</Suspense>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, PartialEq, Clone, Debug)]
|
||||
struct ItemTopParams {
|
||||
id: Option<i64>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemTop() -> impl IntoView {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
let params = use_params::<ItemTopParams>();
|
||||
// map result to an option as the focus isn't error rendering
|
||||
provide_context(Resource::new_blocking(
|
||||
move || params.get().map(|p| p.id),
|
||||
move |id| async move {
|
||||
match id {
|
||||
Err(_) => None,
|
||||
Ok(Some(id)) => get_item(ticket, id).await.ok(),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
));
|
||||
view! {
|
||||
<h4>"<ItemTop/>"</h4>
|
||||
<Outlet/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemOverview() -> impl IntoView {
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let resource = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let item_view = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|GetItemResult(item, names)| view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>{
|
||||
names.into_iter()
|
||||
.map(|name| {
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
let id = item.id;
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/")>
|
||||
"Inspect "{name.clone()}
|
||||
</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_overview += 1);
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h5>"<ItemOverview/>"</h5>
|
||||
<Suspense>
|
||||
{item_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, PartialEq, Clone, Debug)]
|
||||
struct ItemInspectParams {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ItemInspect() -> impl IntoView {
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let params = use_params::<ItemInspectParams>();
|
||||
let res_overview = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let res_inspect = Resource::new_blocking(
|
||||
move || params.get().map(|p| p.path),
|
||||
move |p| async move {
|
||||
// leptos::logging::log!("res_inspect: res_overview.await");
|
||||
let overview = res_overview.await;
|
||||
// leptos::logging::log!("res_inspect: resolved res_overview.await");
|
||||
// let result =
|
||||
match (overview, p) {
|
||||
(Some(item), Ok(Some(path))) => {
|
||||
// leptos::logging::log!("res_inspect: inspect_item().await");
|
||||
inspect_item(ticket, item.0.id, path.clone()).await.ok()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
// ;
|
||||
// leptos::logging::log!("res_inspect: resolved inspect_item().await");
|
||||
// result
|
||||
},
|
||||
);
|
||||
on_cleanup(|| {
|
||||
if let Some(c) = use_context::<WriteSignal<Option<FieldNavCtx>>>() {
|
||||
c.set(None);
|
||||
}
|
||||
});
|
||||
let inspect_view = move || {
|
||||
// leptos::logging::log!("inspect_view closure invoked");
|
||||
Suspend::new(async move {
|
||||
// leptos::logging::log!("inspect_view Suspend::new() called");
|
||||
let result = res_inspect.await.map(|InspectItemResult(item, name, fields)| {
|
||||
// leptos::logging::log!("inspect_view res_inspect awaited");
|
||||
let id = item.id;
|
||||
expect_context::<WriteSignal<Option<FieldNavCtx>>>().set(Some(
|
||||
fields.iter()
|
||||
.map(|field| FieldNavItem {
|
||||
href: format!("/instrumented/item/{id}/{name}/{field}"),
|
||||
text: field.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
));
|
||||
view! {
|
||||
<p>{format!("Inspecting {item:?}")}</p>
|
||||
<ul>{
|
||||
fields.iter()
|
||||
.map(|field| {
|
||||
// FIXME seems like relative link to root for a wildcard isn't
|
||||
// working as expected, so manually construct `a` instead.
|
||||
// let text = format!("Inspect {name}/{field}");
|
||||
// view! {
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// }
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
|
||||
format!("Inspect {name}/{field}")
|
||||
}</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_inspect += 1);
|
||||
// leptos::logging::log!(
|
||||
// "returning result, result.is_some() = {}, count = {}",
|
||||
// result.is_some(),
|
||||
// suspense_counters.get().item_inspect,
|
||||
// );
|
||||
result
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h5>"<ItemInspect/>"</h5>
|
||||
<Suspense>
|
||||
{inspect_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ShowCounters() -> impl IntoView {
|
||||
// There is _weirdness_ in this view. The `Server Calls` counters
|
||||
// will be acquired via the expected mode and be rendered as such.
|
||||
//
|
||||
// However, upon `Reset Counters`, the mode from which the reset
|
||||
// was issued will result in the rendering be reflected as such, so
|
||||
// if the intial state was SSR, resetting under CSR will result in
|
||||
// the CSR counters be rendered after. However for the intents and
|
||||
// purpose for the testing only the CSR is cared for.
|
||||
//
|
||||
// At the end of the day, it is possible to have both these be
|
||||
// separated out, but for the purpose of this test the focus is not
|
||||
// on the SSR side of things (at least until further regression is
|
||||
// discovered that affects SSR directly).
|
||||
let ticket = expect_context::<Ticket>().0;
|
||||
let suspense_counters = expect_context::<RwSignal<SuspenseCounters>>();
|
||||
let reset_counters = ServerAction::<ResetCounters>::new();
|
||||
let res_counter = Resource::new(
|
||||
move || reset_counters.version().get(),
|
||||
move |_| async move {
|
||||
(
|
||||
get_counters(ticket).await,
|
||||
if ticket == 0 { "SSR" } else { "CSR" }.to_string(),
|
||||
ticket,
|
||||
)
|
||||
},
|
||||
);
|
||||
let counter_view = move || {
|
||||
Suspend::new(async move {
|
||||
// ensure current mode and ticket are both updated
|
||||
let (counters, mode, ticket) = res_counter.await;
|
||||
counters.map(|counters| {
|
||||
let clear_suspense_counters = move |_| {
|
||||
suspense_counters.update(|c| {
|
||||
// leptos::logging::log!("resetting suspense counters");
|
||||
*c = SuspenseCounters::default();
|
||||
});
|
||||
};
|
||||
view! {
|
||||
<h3 id="server-calls">"Server Calls ("{mode}")"</h3>
|
||||
<dl>
|
||||
<dt>"list_items"</dt>
|
||||
<dd id="list_items">{counters.list_items}</dd>
|
||||
<dt>"get_item"</dt>
|
||||
<dd id="get_item">{counters.get_item}</dd>
|
||||
<dt>"inspect_item_root"</dt>
|
||||
<dd id="inspect_item_root">{counters.inspect_item_root}</dd>
|
||||
<dt>"inspect_item_field"</dt>
|
||||
<dd id="inspect_item_field">{counters.inspect_item_field}</dd>
|
||||
</dl>
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-counters"
|
||||
type="submit"
|
||||
value="Reset Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<h2>"Counters"</h2>
|
||||
|
||||
<h3 id="suspend-calls">"Suspend Calls"</h3>
|
||||
{move || suspense_counters.with(|c| view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
})}
|
||||
|
||||
<Suspense>
|
||||
{counter_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct FieldNavItem {
|
||||
pub href: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct FieldNavCtx(pub Option<Vec<FieldNavItem>>);
|
||||
|
||||
impl From<Vec<FieldNavItem>> for FieldNavCtx {
|
||||
fn from(item: Vec<FieldNavItem>) -> Self {
|
||||
Self(Some(item))
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FieldNavPortlet() -> impl IntoView {
|
||||
let ctx = expect_context::<ReadSignal<Option<FieldNavCtx>>>();
|
||||
move || {
|
||||
let ctx = ctx.get();
|
||||
ctx.map(|ctx| {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>{
|
||||
ctx.0.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! {
|
||||
<A href=href>{text}</A>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
})
|
||||
}</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provide_field_nav_portlet_context() {
|
||||
// wrapping the Ctx in an Option allows better ergonomics whenever it isn't needed
|
||||
let (ctx, set_ctx) = signal(None::<FieldNavCtx>);
|
||||
provide_context(ctx);
|
||||
provide_context(set_ctx);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod app;
|
||||
mod instrumented;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
||||
@@ -41,7 +41,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.service(Files::new("/", site_root))
|
||||
})
|
||||
.bind(addr)?
|
||||
.workers(1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title 'Leptos + Tailwindcss'", async ({ page }) => {
|
||||
test("should see the welcome message", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Leptos + Tailwindcss");
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
});
|
||||
|
||||
@@ -22,29 +22,24 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Home() -> impl IntoView {
|
||||
let (value, set_value) = signal(0);
|
||||
let (count, set_count) = signal(0);
|
||||
|
||||
// thanks to https://tailwindcomponents.com/component/blue-buttons-example for the showcase layout
|
||||
view! {
|
||||
<Title text="Leptos + Tailwindcss"/>
|
||||
<main>
|
||||
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
|
||||
<div class="flex flex-row-reverse flex-wrap m-auto">
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"+"
|
||||
</button>
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_value.update(|value| *value -= 1)
|
||||
class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white"
|
||||
class:invisible=move || {value.get() < 1}
|
||||
>
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<main class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count.get() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count.get().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.service(Files::new("/", site_root))
|
||||
.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title 'Leptos + Tailwindcss'", async ({ page }) => {
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
|
||||
await expect(page).toHaveTitle("Leptos + Tailwindcss");
|
||||
await expect(page).toHaveTitle("Welcome to Leptos");
|
||||
|
||||
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
|
||||
});
|
||||
|
||||
@@ -54,11 +54,7 @@ fn Home() -> impl IntoView {
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_value.update(|value| *value -= 1)
|
||||
class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white"
|
||||
class:invisible=move || {value.get() < 1}
|
||||
>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
content: ["*.html", "./src/**/*.rs",],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title 'Leptos + Tailwindcss'", async ({ page }) => {
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
await page.goto("http://localhost:8080/");
|
||||
|
||||
await expect(page).toHaveTitle("Leptos + Tailwindcss");
|
||||
await expect(page).toHaveTitle("Leptos • Counter with Tailwind");
|
||||
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
});
|
||||
|
||||
@@ -22,29 +22,24 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Home() -> impl IntoView {
|
||||
let (value, set_value) = signal(0);
|
||||
let (count, set_count) = signal(0);
|
||||
|
||||
// thanks to https://tailwindcomponents.com/component/blue-buttons-example for the showcase layout
|
||||
view! {
|
||||
<Title text="Leptos + Tailwindcss"/>
|
||||
<main>
|
||||
<div class="bg-gradient-to-tl from-blue-800 to-blue-500 text-white font-mono flex flex-col min-h-screen">
|
||||
<div class="flex flex-row-reverse flex-wrap m-auto">
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1) class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white">
|
||||
"+"
|
||||
</button>
|
||||
<button class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-800 border-blue-900 text-white">
|
||||
{value}
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| set_value.update(|value| *value -= 1)
|
||||
class="rounded px-3 py-2 m-1 border-b-4 border-l-2 shadow-lg bg-blue-700 border-blue-800 text-white"
|
||||
class:invisible=move || {value.get() < 1}
|
||||
>
|
||||
"-"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count.get() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count.get().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
transform: {
|
||||
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn TimerDemo() -> impl IntoView {
|
||||
pub fn use_interval<T, F>(interval_millis: T, f: F)
|
||||
where
|
||||
F: Fn() + Clone + 'static,
|
||||
T: Into<Signal<u64>> + 'static,
|
||||
T: Into<MaybeSignal<u64>> + 'static,
|
||||
{
|
||||
let interval_millis = interval_millis.into();
|
||||
Effect::new(move |prev_handle: Option<IntervalHandle>| {
|
||||
|
||||
@@ -59,7 +59,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -16,15 +16,15 @@ leptos_router = { path = "../../router" }
|
||||
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "2.0"
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -9,7 +9,7 @@ use leptos::{
|
||||
hydration::{AutoReload, HydrationScripts},
|
||||
prelude::*,
|
||||
};
|
||||
use tower::util::ServiceExt;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_or_index_handler(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.0-rc1"
|
||||
version = "0.2.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,12 +12,12 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
futures = "0.3.30"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { version = "0.2.95", optional = true }
|
||||
js-sys = { version = "0.3.72", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
wasm-bindgen = { version = "0.2.93", optional = true }
|
||||
js-sys = { version = "0.3.69", optional = true }
|
||||
once_cell = "1.19"
|
||||
pin-project-lite = "0.2.14"
|
||||
|
||||
[features]
|
||||
browser = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
|
||||
@@ -44,18 +44,6 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
|
||||
/// from the server to the client.
|
||||
pub struct SerializedDataId(usize);
|
||||
|
||||
impl SerializedDataId {
|
||||
/// Create a new instance of [`SerializedDataId`].
|
||||
pub fn new(id: usize) -> Self {
|
||||
SerializedDataId(id)
|
||||
}
|
||||
|
||||
/// Consume into the inner usize identifier.
|
||||
pub fn into_inner(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerializedDataId> for ErrorId {
|
||||
fn from(value: SerializedDataId) -> Self {
|
||||
value.0.into()
|
||||
|
||||
@@ -58,27 +58,6 @@ impl SsrSharedContext {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the data buffers, awaiting all async resources,
|
||||
/// returning both sync and async buffers.
|
||||
/// Useful to implement custom hydration contexts.
|
||||
///
|
||||
/// WARNING: this will clear the internal buffers, it should only be called once.
|
||||
/// A second call would return an empty `vec![]`.
|
||||
pub async fn consume_buffers(&self) -> Vec<(SerializedDataId, String)> {
|
||||
let sync_data = mem::take(&mut *self.sync_buf.write().or_poisoned());
|
||||
let async_data = mem::take(&mut *self.async_buf.write().or_poisoned());
|
||||
|
||||
let mut all_data = Vec::new();
|
||||
for resolved in sync_data {
|
||||
all_data.push((resolved.0, resolved.1));
|
||||
}
|
||||
for (id, fut) in async_data {
|
||||
let data = fut.await;
|
||||
all_data.push((id, data));
|
||||
}
|
||||
all_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SsrSharedContext {
|
||||
|
||||
@@ -9,10 +9,10 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.9"
|
||||
actix-http = "3.8"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.9"
|
||||
futures = "0.3.31"
|
||||
actix-web = "4.8"
|
||||
futures = "0.3.30"
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
@@ -24,7 +24,7 @@ server_fn = { workspace = true, features = ["actix"] }
|
||||
serde_json = "1.0"
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.41", features = ["rt", "fs"] }
|
||||
tokio = { version = "1.39", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Actix.
|
||||
//!
|
||||
@@ -10,6 +9,7 @@
|
||||
use actix_files::NamedFile;
|
||||
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
|
||||
use actix_web::{
|
||||
body::BoxBody,
|
||||
dev::{ServiceFactory, ServiceRequest},
|
||||
http::header,
|
||||
test,
|
||||
@@ -35,7 +35,7 @@ use leptos_router::{
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
@@ -44,7 +44,6 @@ use server_fn::{
|
||||
redirect::REDIRECT_HEADER, request::actix::ActixRequest, ServerFnError,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
@@ -56,10 +55,8 @@ use std::{
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseParts {
|
||||
/// If provided, this will overwrite any other status code for this response.
|
||||
pub status: Option<StatusCode>,
|
||||
/// The map of headers that should be added to the response.
|
||||
pub headers: header::HeaderMap,
|
||||
pub status: Option<StatusCode>,
|
||||
}
|
||||
|
||||
impl ResponseParts {
|
||||
@@ -88,12 +85,10 @@ impl ResponseParts {
|
||||
pub struct Request(SendWrapper<HttpRequest>);
|
||||
|
||||
impl Request {
|
||||
/// Wraps an existing Actix request.
|
||||
pub fn new(req: &HttpRequest) -> Self {
|
||||
Self(SendWrapper::new(req.clone()))
|
||||
}
|
||||
|
||||
/// Consumes the wrapper and returns the inner Actix request.
|
||||
pub fn into_inner(self) -> HttpRequest {
|
||||
self.0.take()
|
||||
}
|
||||
@@ -303,7 +298,7 @@ pub fn redirect(path: &str) {
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
@@ -330,7 +325,7 @@ pub fn handle_server_fns() -> Route {
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
@@ -463,7 +458,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
@@ -533,7 +528,8 @@ where
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
@@ -597,7 +593,9 @@ where
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
@@ -621,7 +619,9 @@ where
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
@@ -656,7 +656,9 @@ where
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
@@ -688,7 +690,7 @@ where
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
@@ -721,7 +723,9 @@ where
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [Request]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
@@ -896,7 +900,7 @@ trait ActixPath {
|
||||
fn to_actix_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl ActixPath for Vec<PathSegment> {
|
||||
impl ActixPath for &[PathSegment] {
|
||||
fn to_actix_path(&self) -> String {
|
||||
let mut path = String::new();
|
||||
for segment in self.iter() {
|
||||
@@ -918,14 +922,6 @@ impl ActixPath for Vec<PathSegment> {
|
||||
path.push_str(":.*}");
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!(
|
||||
"to_axum_path should only be called on expanded \
|
||||
paths, which do not have OptionalParam any longer"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
@@ -939,38 +935,25 @@ pub struct ActixRouteListing {
|
||||
mode: SsrMode,
|
||||
methods: Vec<leptos_router::Method>,
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
exclude: bool,
|
||||
}
|
||||
|
||||
trait IntoRouteListing: Sized {
|
||||
fn into_route_listing(self) -> Vec<ActixRouteListing>;
|
||||
}
|
||||
|
||||
impl IntoRouteListing for RouteListing {
|
||||
fn into_route_listing(self) -> Vec<ActixRouteListing> {
|
||||
self.path()
|
||||
.to_vec()
|
||||
.expand_optionals()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let path = path.to_actix_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = self.mode();
|
||||
let methods = self.methods().collect();
|
||||
let regenerate = self.regenerate().into();
|
||||
ActixRouteListing {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
exclude: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
impl From<RouteListing> for ActixRouteListing {
|
||||
fn from(value: RouteListing) -> Self {
|
||||
let path = value.path().to_actix_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,7 +970,6 @@ impl ActixRouteListing {
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
regenerate: regenerate.into(),
|
||||
exclude: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,37 +1027,27 @@ where
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.flat_map(IntoRouteListing::into_route_listing)
|
||||
.map(ActixRouteListing::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let routes = if routes.is_empty() {
|
||||
vec![ActixRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = &excluded_routes {
|
||||
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
};
|
||||
|
||||
let excluded =
|
||||
excluded_routes
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|path| ActixRouteListing {
|
||||
path,
|
||||
mode: Default::default(),
|
||||
methods: Vec::new(),
|
||||
regenerate: Vec::new(),
|
||||
exclude: true,
|
||||
});
|
||||
|
||||
(routes.into_iter().chain(excluded).collect(), generator)
|
||||
(
|
||||
if routes.is_empty() {
|
||||
vec![ActixRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = excluded_routes {
|
||||
routes
|
||||
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
},
|
||||
generator,
|
||||
)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
@@ -1314,12 +1286,14 @@ where
|
||||
web::get().to(handler)
|
||||
}
|
||||
|
||||
pub enum DataResponse<T> {
|
||||
Data(T),
|
||||
Response(actix_web::dev::Response<BoxBody>),
|
||||
}
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes {
|
||||
/// Adds routes to the Axum router that have either
|
||||
/// 1) been generated by `leptos_router`, or
|
||||
/// 2) handle a server function.
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1328,12 +1302,6 @@ pub trait LeptosRoutes {
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
/// Adds routes to the Axum router that have either
|
||||
/// 1) been generated by `leptos_router`, or
|
||||
/// 2) handle a server function.
|
||||
///
|
||||
/// Runs `additional_context` to provide additional data to the reactive system via context,
|
||||
/// when handling a route.
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1385,24 +1353,15 @@ where
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
let excluded = paths
|
||||
.iter()
|
||||
.filter(|&p| p.exclude)
|
||||
.map(|p| p.path.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// register server functions first to allow for wildcard route in Leptos's Router
|
||||
for (path, _) in server_fn::actix::server_fn_paths() {
|
||||
if !excluded.contains(path) {
|
||||
let additional_context = additional_context.clone();
|
||||
let handler =
|
||||
handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
}
|
||||
let additional_context = additional_context.clone();
|
||||
let handler = handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
}
|
||||
|
||||
// register routes defined in Leptos's Router
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
for listing in paths.iter() {
|
||||
let path = listing.path();
|
||||
let mode = listing.mode();
|
||||
|
||||
@@ -1498,24 +1457,15 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
let excluded = paths
|
||||
.iter()
|
||||
.filter(|&p| p.exclude)
|
||||
.map(|p| p.path.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// register server functions first to allow for wildcard route in Leptos's Router
|
||||
for (path, _) in server_fn::actix::server_fn_paths() {
|
||||
if !excluded.contains(path) {
|
||||
let additional_context = additional_context.clone();
|
||||
let handler =
|
||||
handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
}
|
||||
let additional_context = additional_context.clone();
|
||||
let handler = handle_server_fns_with_context(additional_context);
|
||||
router = router.route(path, handler);
|
||||
}
|
||||
|
||||
// register routes defined in Leptos's Router
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
for listing in paths.iter() {
|
||||
let path = listing.path();
|
||||
let mode = listing.mode();
|
||||
|
||||
@@ -1604,10 +1554,7 @@ where
|
||||
ServerFnError::new("HttpRequest should have been provided via context")
|
||||
})?;
|
||||
|
||||
SendWrapper::new(async move {
|
||||
T::extract(&req)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
})
|
||||
.await
|
||||
T::extract(&req)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.7.8", default-features = false, features = [
|
||||
axum = { version = "0.7.5", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.31"
|
||||
futures = "0.3.30"
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -24,14 +24,14 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.41", default-features = false }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
tower-http = "0.6.1"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = "0.5.2"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7.8"
|
||||
tokio = { version = "1.41", features = ["net", "rt-multi-thread"] }
|
||||
axum = "0.7.5"
|
||||
tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! Provides functions to easily integrate Leptos with Axum.
|
||||
//!
|
||||
//! ## JS Fetch Integration
|
||||
@@ -16,7 +14,7 @@
|
||||
//! - `default`: supports running in a typical native Tokio/Axum environment
|
||||
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
|
||||
//! environment
|
||||
//! - `islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
|
||||
//! - `experimental-islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
|
||||
//!
|
||||
//! ### Important Note
|
||||
//! Prior to 0.5, using `default-features = false` on `leptos_axum` simply did nothing. Now, it actively
|
||||
@@ -65,9 +63,10 @@ use leptos_meta::ServerMetaContext;
|
||||
#[cfg(feature = "default")]
|
||||
use leptos_router::static_routes::ResolvedStaticPath;
|
||||
use leptos_router::{
|
||||
components::provide_server_redirect, location::RequestUrl,
|
||||
static_routes::RegenerationFn, ExpandOptionals, PathSegment, RouteList,
|
||||
RouteListing, SsrMode,
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, StaticParamsMap},
|
||||
PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -75,7 +74,7 @@ use parking_lot::RwLock;
|
||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
#[cfg(feature = "default")]
|
||||
use std::path::Path;
|
||||
use std::{collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
#[cfg(feature = "default")]
|
||||
use tower::util::ServiceExt;
|
||||
#[cfg(feature = "default")]
|
||||
@@ -86,9 +85,7 @@ use tower_http::services::ServeDir;
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseParts {
|
||||
/// If provided, this will overwrite any other status code for this response.
|
||||
pub status: Option<StatusCode>,
|
||||
/// The map of headers that should be added to the response.
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
|
||||
@@ -435,7 +432,6 @@ async fn handle_server_fns_inner(
|
||||
.expect("could not build Response")
|
||||
}
|
||||
|
||||
/// A stream of bytes of HTML.
|
||||
pub type PinnedHtmlStream =
|
||||
Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
|
||||
|
||||
@@ -1211,6 +1207,32 @@ where
|
||||
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
|
||||
}
|
||||
|
||||
/// Builds all routes that have been defined using [`StaticRoute`].
|
||||
#[allow(unused)]
|
||||
pub async fn build_static_routes<IV>(
|
||||
options: &LeptosOptions,
|
||||
app_fn: impl Fn() -> IV + 'static + Send + Clone,
|
||||
routes: &[RouteListing],
|
||||
static_data_map: StaticParamsMap,
|
||||
) where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
todo!()
|
||||
/*
|
||||
let options = options.clone();
|
||||
let routes = routes.to_owned();
|
||||
spawn_task!(async move {
|
||||
leptos_router::build_static_routes(
|
||||
&options,
|
||||
app_fn,
|
||||
&routes,
|
||||
&static_data_map,
|
||||
)
|
||||
.await
|
||||
.expect("could not build static routes")
|
||||
});*/
|
||||
}
|
||||
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
|
||||
@@ -1241,38 +1263,25 @@ pub struct AxumRouteListing {
|
||||
methods: Vec<leptos_router::Method>,
|
||||
#[allow(unused)]
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
exclude: bool,
|
||||
}
|
||||
|
||||
trait IntoRouteListing: Sized {
|
||||
fn into_route_listing(self) -> Vec<AxumRouteListing>;
|
||||
}
|
||||
|
||||
impl IntoRouteListing for RouteListing {
|
||||
fn into_route_listing(self) -> Vec<AxumRouteListing> {
|
||||
self.path()
|
||||
.to_vec()
|
||||
.expand_optionals()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let path = path.to_axum_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = self.mode();
|
||||
let methods = self.methods().collect();
|
||||
let regenerate = self.regenerate().into();
|
||||
AxumRouteListing {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
exclude: false,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
impl From<RouteListing> for AxumRouteListing {
|
||||
fn from(value: RouteListing) -> Self {
|
||||
let path = value.path().to_axum_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1289,7 +1298,6 @@ impl AxumRouteListing {
|
||||
mode,
|
||||
methods: methods.into_iter().collect(),
|
||||
regenerate: regenerate.into(),
|
||||
exclude: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1352,36 +1360,27 @@ where
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.flat_map(IntoRouteListing::into_route_listing)
|
||||
.map(AxumRouteListing::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let routes = if routes.is_empty() {
|
||||
vec![AxumRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = &excluded_routes {
|
||||
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
};
|
||||
let excluded =
|
||||
excluded_routes
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|path| AxumRouteListing {
|
||||
path,
|
||||
mode: Default::default(),
|
||||
methods: Vec::new(),
|
||||
regenerate: Vec::new(),
|
||||
exclude: true,
|
||||
});
|
||||
|
||||
(routes.into_iter().chain(excluded).collect(), generator)
|
||||
(
|
||||
if routes.is_empty() {
|
||||
vec![AxumRouteListing::new(
|
||||
"/".to_string(),
|
||||
Default::default(),
|
||||
[leptos_router::Method::Get],
|
||||
vec![],
|
||||
)]
|
||||
} else {
|
||||
// Routes to exclude from auto generation
|
||||
if let Some(excluded_routes) = excluded_routes {
|
||||
routes
|
||||
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
|
||||
}
|
||||
routes
|
||||
},
|
||||
generator,
|
||||
)
|
||||
}
|
||||
|
||||
/// Allows generating any prerendered routes.
|
||||
@@ -1661,9 +1660,6 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
LeptosOptions: FromRef<S>,
|
||||
{
|
||||
/// Adds routes to the Axum router that have either
|
||||
/// 1) been generated by `leptos_router`, or
|
||||
/// 2) handle a server function.
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
options: &S,
|
||||
@@ -1673,12 +1669,6 @@ where
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
/// Adds routes to the Axum router that have either
|
||||
/// 1) been generated by `leptos_router`, or
|
||||
/// 2) handle a server function.
|
||||
///
|
||||
/// Runs `additional_context` to provide additional data to the reactive system via context,
|
||||
/// when handling a route.
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
options: &S,
|
||||
@@ -1689,8 +1679,6 @@ where
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
/// Extends the Axum router with the given paths, and handles the requests with the given
|
||||
/// handler.
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
@@ -1705,7 +1693,7 @@ trait AxumPath {
|
||||
fn to_axum_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl AxumPath for Vec<PathSegment> {
|
||||
impl AxumPath for &[PathSegment] {
|
||||
fn to_axum_path(&self) -> String {
|
||||
let mut path = String::new();
|
||||
for segment in self.iter() {
|
||||
@@ -1725,14 +1713,6 @@ impl AxumPath for Vec<PathSegment> {
|
||||
path.push_str(s);
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!(
|
||||
"to_axum_path should only be called on expanded \
|
||||
paths, which do not have OptionalParam any longer"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
@@ -1788,41 +1768,32 @@ where
|
||||
|
||||
let mut router = self;
|
||||
|
||||
let excluded = paths
|
||||
.iter()
|
||||
.filter(|&p| p.exclude)
|
||||
.map(|p| p.path.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// register server functions
|
||||
for (path, method) in server_fn::axum::server_fn_paths() {
|
||||
let cx_with_state = cx_with_state.clone();
|
||||
let handler = move |req: Request<Body>| async move {
|
||||
handle_server_fns_with_context(cx_with_state, req).await
|
||||
};
|
||||
|
||||
if !excluded.contains(path) {
|
||||
router = router.route(
|
||||
path,
|
||||
match method {
|
||||
Method::GET => get(handler),
|
||||
Method::POST => post(handler),
|
||||
Method::PUT => put(handler),
|
||||
Method::DELETE => delete(handler),
|
||||
Method::PATCH => patch(handler),
|
||||
_ => {
|
||||
panic!(
|
||||
"Unsupported server function HTTP method: \
|
||||
{method:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
router = router.route(
|
||||
path,
|
||||
match method {
|
||||
Method::GET => get(handler),
|
||||
Method::POST => post(handler),
|
||||
Method::PUT => put(handler),
|
||||
Method::DELETE => delete(handler),
|
||||
Method::PATCH => patch(handler),
|
||||
_ => {
|
||||
panic!(
|
||||
"Unsupported server function HTTP method: \
|
||||
{method:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// register router paths
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
for listing in paths.iter() {
|
||||
let path = listing.path();
|
||||
|
||||
for method in listing.methods() {
|
||||
@@ -1931,7 +1902,7 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
for listing in paths.iter().filter(|p| !p.exclude) {
|
||||
for listing in paths.iter() {
|
||||
for method in listing.methods() {
|
||||
router = router.route(
|
||||
listing.path(),
|
||||
@@ -2003,10 +1974,6 @@ where
|
||||
.map_err(|e| ServerFnError::ServerError(format!("{e:?}")))
|
||||
}
|
||||
|
||||
/// A reasonable handler for serving static files (like JS/WASM/CSS) and 404 errors.
|
||||
///
|
||||
/// This is provided as a convenience, but is a fairly simple function. If you need to adapt it,
|
||||
/// simply reuse the source code of this function in your own application.
|
||||
#[cfg(feature = "default")]
|
||||
pub fn file_and_error_handler<S, IV>(
|
||||
shell: fn(LeptosOptions) -> IV,
|
||||
@@ -2026,7 +1993,7 @@ where
|
||||
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
|
||||
Box::pin(async move {
|
||||
let options = LeptosOptions::from_ref(&options);
|
||||
let res = get_static_file(uri, &options.site_root, req.headers());
|
||||
let res = get_static_file(uri, &options.site_root);
|
||||
let res = res.await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
@@ -2060,26 +2027,14 @@ where
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
use axum::http::header::ACCEPT_ENCODING;
|
||||
|
||||
let req = Request::builder().uri(uri);
|
||||
|
||||
let req = match headers.get(ACCEPT_ENCODING) {
|
||||
Some(value) => req.header(ACCEPT_ENCODING, value),
|
||||
None => req,
|
||||
};
|
||||
|
||||
let req = req.body(Body::empty()).unwrap();
|
||||
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)
|
||||
.precompressed_gzip()
|
||||
.precompressed_br()
|
||||
.oneshot(req)
|
||||
.await
|
||||
{
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.31"
|
||||
futures = "0.3.30"
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
any_spawner = { workspace = true, features = ["wasm-bindgen", "futures-executor"] }
|
||||
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
cfg-if = "1.0"
|
||||
hydration_context = { workspace = true }
|
||||
@@ -28,11 +28,11 @@ paste = "1.0"
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
tachys = { workspace = true, features = ["reactive_graph", "reactive_stores", "oco"] }
|
||||
thiserror = "2.0"
|
||||
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
|
||||
thiserror = "1.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
typed-builder = "0.20.0"
|
||||
typed-builder-macro = "0.20.0"
|
||||
typed-builder = "0.19.1"
|
||||
typed-builder-macro = "0.19.1"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = [
|
||||
@@ -40,15 +40,15 @@ server_fn = { workspace = true, features = [
|
||||
"browser",
|
||||
"url",
|
||||
] }
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = "0.2.95"
|
||||
wasm-bindgen = "=0.2.93"
|
||||
serde_qs = "0.13.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
futures = "0.3.30"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
@@ -86,7 +86,7 @@ tracing = [
|
||||
]
|
||||
nonce = ["base64", "rand"]
|
||||
spin = ["leptos-spin-macro"]
|
||||
islands = ["leptos_macro/islands", "dep:serde_json"]
|
||||
experimental-islands = ["leptos_macro/experimental-islands", "dep:serde_json"]
|
||||
trace-component-props = [
|
||||
"leptos_macro/trace-component-props",
|
||||
"leptos_dom/trace-component-props"
|
||||
@@ -104,7 +104,7 @@ denylist = [
|
||||
"rkyv", # was causing clippy issues on nightly
|
||||
"trace-component-props",
|
||||
"spin",
|
||||
"islands",
|
||||
"experimental-islands",
|
||||
]
|
||||
skip_feature_sets = [
|
||||
["csr", "ssr"],
|
||||
|
||||
@@ -223,14 +223,14 @@ mod tests {
|
||||
#[test]
|
||||
fn clone_callback() {
|
||||
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback;
|
||||
let _cloned = callback.clone();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_unsync_callback() {
|
||||
let callback =
|
||||
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback;
|
||||
let _cloned = callback.clone();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -228,7 +228,6 @@ impl ViewFnOnce {
|
||||
pub struct TypedChildren<T>(Box<dyn FnOnce() -> View<T> + Send>);
|
||||
|
||||
impl<T> TypedChildren<T> {
|
||||
/// Extracts the inner `children` function.
|
||||
pub fn into_inner(self) -> impl FnOnce() -> View<T> + Send {
|
||||
self.0
|
||||
}
|
||||
@@ -257,7 +256,6 @@ impl<T> Debug for TypedChildrenMut<T> {
|
||||
}
|
||||
|
||||
impl<T> TypedChildrenMut<T> {
|
||||
/// Extracts the inner `children` function.
|
||||
pub fn into_inner(self) -> impl FnMut() -> View<T> + Send {
|
||||
self.0
|
||||
}
|
||||
@@ -286,7 +284,6 @@ impl<T> Debug for TypedChildrenFn<T> {
|
||||
}
|
||||
|
||||
impl<T> TypedChildrenFn<T> {
|
||||
/// Extracts the inner `children` function.
|
||||
pub fn into_inner(self) -> Arc<dyn Fn() -> View<T> + Send + Sync> {
|
||||
self.0
|
||||
}
|
||||
|
||||
@@ -251,16 +251,12 @@ where
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
}
|
||||
|
||||
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FromFormDataError {
|
||||
/// Could not find a `<form>` connected to the event.
|
||||
#[error("Could not find <form> connected to event.")]
|
||||
MissingForm(Event),
|
||||
/// Could not create `FormData` from the form.
|
||||
#[error("Could not create FormData from <form>: {0:?}")]
|
||||
FormData(JsValue),
|
||||
/// Failed to deserialize this Rust type from the form data.
|
||||
#[error("Deserialization error: {0:?}")]
|
||||
Deserialization(serde_qs::Error),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(function (root, pkg_path, output_name, wasm_output_name) {
|
||||
import(`${root}/${pkg_path}/${output_name}.js`)
|
||||
.then(mod => {
|
||||
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
|
||||
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
|
||||
mod.hydrate();
|
||||
});
|
||||
})
|
||||
|
||||
@@ -4,15 +4,9 @@ use crate::prelude::*;
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_macro::{component, view};
|
||||
|
||||
/// Inserts auto-reloading code used in `cargo-leptos`.
|
||||
///
|
||||
/// This should be included in the `<head>` of your application shell during development.
|
||||
#[component]
|
||||
pub fn AutoReload(
|
||||
/// Whether the file-watching feature should be disabled.
|
||||
#[prop(optional)]
|
||||
disable_watch: bool,
|
||||
/// Configuration options for this project.
|
||||
#[prop(optional)] disable_watch: bool,
|
||||
options: LeptosOptions,
|
||||
) -> impl IntoView {
|
||||
(!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| {
|
||||
@@ -40,16 +34,10 @@ pub fn AutoReload(
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts hydration scripts that add interactivity to your server-rendered HTML.
|
||||
///
|
||||
/// This should be included in the `<head>` of your application shell.
|
||||
#[component]
|
||||
pub fn HydrationScripts(
|
||||
/// Configuration options for this project.
|
||||
options: LeptosOptions,
|
||||
/// Should be `true` to hydrate in `islands` mode.
|
||||
#[prop(optional)]
|
||||
islands: bool,
|
||||
#[prop(optional)] islands: bool,
|
||||
/// A base url, not including a trailing slash
|
||||
#[prop(optional, into)]
|
||||
root: Option<String>,
|
||||
@@ -62,7 +50,7 @@ pub fn HydrationScripts(
|
||||
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.join(options.hash_file.as_ref());
|
||||
.join(&options.hash_file);
|
||||
if hash_path.exists() {
|
||||
let hashes = std::fs::read_to_string(&hash_path)
|
||||
.expect("failed to read hash file");
|
||||
|
||||
@@ -9,7 +9,6 @@ use tachys::{
|
||||
},
|
||||
};
|
||||
|
||||
/// A wrapper for any kind of view.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct View<T>
|
||||
where
|
||||
@@ -21,7 +20,6 @@ where
|
||||
}
|
||||
|
||||
impl<T> View<T> {
|
||||
/// Wraps the view.
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
@@ -30,12 +28,10 @@ impl<T> View<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwraps the view, returning the inner type.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Adds a view marker, which is used for hot-reloading and debug purposes.
|
||||
#[inline(always)]
|
||||
pub fn with_view_marker(
|
||||
#[allow(unused_mut)] // used in debug
|
||||
@@ -51,12 +47,10 @@ impl<T> View<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait that is implemented for types that can be rendered.
|
||||
pub trait IntoView
|
||||
where
|
||||
Self: Sized + Render + RenderHtml + Send,
|
||||
{
|
||||
/// Wraps the inner type.
|
||||
fn into_view(self) -> View<Self>;
|
||||
}
|
||||
|
||||
@@ -194,15 +188,9 @@ impl<T: AddAnyAttr> AddAnyAttr for View<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects some iterator of views into a list, so they can be rendered.
|
||||
///
|
||||
/// This is a shorthand for `.collect::<Vec<_>>()`, and allows any iterator of renderable
|
||||
/// items to be collected into a renderable collection.
|
||||
pub trait CollectView {
|
||||
/// The inner view type.
|
||||
type View: IntoView;
|
||||
|
||||
/// Collects the iterator into a list of views.
|
||||
fn collect_view(self) -> Vec<Self::View>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#![deny(missing_docs)]
|
||||
#!rdeny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! # About Leptos
|
||||
//!
|
||||
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
|
||||
@@ -164,7 +163,7 @@ pub mod prelude {
|
||||
form::*, hydration::*, into_view::*, mount::*, suspense::*,
|
||||
};
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_dom::helpers::*;
|
||||
pub use leptos_dom::{helpers::*, *};
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_server::*;
|
||||
pub use oco_ref::*;
|
||||
@@ -288,7 +287,6 @@ pub mod logging {
|
||||
pub use leptos_dom::{debug_warn, error, log, warn};
|
||||
}
|
||||
|
||||
/// Utilities for working with asynchronous tasks.
|
||||
pub mod task {
|
||||
pub use any_spawner::Executor;
|
||||
use std::future::Future;
|
||||
@@ -318,10 +316,10 @@ pub mod task {
|
||||
}
|
||||
|
||||
// these reexports are used in islands
|
||||
#[cfg(feature = "islands")]
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
#[doc(hidden)]
|
||||
pub use serde;
|
||||
#[cfg(feature = "islands")]
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
#[doc(hidden)]
|
||||
pub use serde_json;
|
||||
#[cfg(feature = "tracing")]
|
||||
|
||||
@@ -87,12 +87,7 @@ use throw_error::ErrorHookFuture;
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn Suspense<Chil>(
|
||||
/// A function that returns a fallback that will be shown while resources are still loading.
|
||||
/// By default this is an empty view.
|
||||
#[prop(optional, into)]
|
||||
fallback: ViewFnOnce,
|
||||
/// Children will be rendered once initially to catch any resource reads, then hidden until all
|
||||
/// data have loaded.
|
||||
#[prop(optional, into)] fallback: ViewFnOnce,
|
||||
children: TypedChildren<Chil>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
|
||||
2851
leptos/tests/test_examples/suspense-tests/Cargo.lock
generated
2851
leptos/tests/test_examples/suspense-tests/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,18 +10,18 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.14.1", default-features = false, features = [
|
||||
config = { version = "0.14.0", default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] }
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = "2.0"
|
||||
typed-builder = "0.20.0"
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
typed-builder = "0.19.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.41", features = ["rt", "macros"] }
|
||||
tempfile = "3.14"
|
||||
tokio = { version = "1.39", features = ["rt", "macros"] }
|
||||
tempfile = "3.12"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -5,9 +5,7 @@ pub mod errors;
|
||||
use crate::errors::LeptosConfigError;
|
||||
use config::{Case, Config, File, FileFormat};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
env::VarError, fs, net::SocketAddr, path::Path, str::FromStr, sync::Arc,
|
||||
};
|
||||
use std::{env::VarError, fs, net::SocketAddr, path::Path, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
@@ -27,17 +25,17 @@ pub struct ConfFile {
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
pub output_name: Arc<str>,
|
||||
pub output_name: String,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
#[builder(setter(into), default=default_site_root())]
|
||||
#[serde(default = "default_site_root")]
|
||||
pub site_root: Arc<str>,
|
||||
pub site_root: String,
|
||||
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
|
||||
/// By default, wasm-bindgen puts them in `pkg`.
|
||||
#[builder(setter(into), default=default_site_pkg_dir())]
|
||||
#[serde(default = "default_site_pkg_dir")]
|
||||
pub site_pkg_dir: Arc<str>,
|
||||
pub site_pkg_dir: String,
|
||||
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
|
||||
/// things based on the deployment environment
|
||||
/// I recommend passing in the result of `env::var("LEPTOS_ENV")`
|
||||
@@ -68,11 +66,11 @@ pub struct LeptosOptions {
|
||||
/// The path of a custom 404 Not Found page to display when statically serving content, defaults to `site_root/404.html`
|
||||
#[builder(default = default_not_found_path())]
|
||||
#[serde(default = "default_not_found_path")]
|
||||
pub not_found_path: Arc<str>,
|
||||
pub not_found_path: String,
|
||||
/// The file name of the hash text file generated by cargo-leptos. Defaults to `hash.txt`.
|
||||
#[builder(default = default_hash_file_name())]
|
||||
#[serde(default = "default_hash_file_name")]
|
||||
pub hash_file: Arc<str>,
|
||||
pub hash_file: String,
|
||||
/// If true, hashes will be generated for all files in the site_root and added to their file names.
|
||||
/// Defaults to `true`.
|
||||
#[builder(default = default_hash_files())]
|
||||
@@ -98,9 +96,9 @@ impl LeptosOptions {
|
||||
);
|
||||
}
|
||||
Ok(LeptosOptions {
|
||||
output_name: output_name.into(),
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?.into(),
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?.into(),
|
||||
output_name,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
|
||||
.parse()?,
|
||||
@@ -115,10 +113,8 @@ impl LeptosOptions {
|
||||
reload_ws_protocol: ws_from_str(
|
||||
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
|
||||
)?,
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?
|
||||
.into(),
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
|
||||
.into(),
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?,
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
})
|
||||
}
|
||||
@@ -130,16 +126,16 @@ impl Default for LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> Arc<str> {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_").into()
|
||||
fn default_output_name() -> String {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_")
|
||||
}
|
||||
|
||||
fn default_site_root() -> Arc<str> {
|
||||
".".into()
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
}
|
||||
|
||||
fn default_site_pkg_dir() -> Arc<str> {
|
||||
"pkg".into()
|
||||
fn default_site_pkg_dir() -> String {
|
||||
"pkg".to_string()
|
||||
}
|
||||
|
||||
fn default_env() -> Env {
|
||||
@@ -154,12 +150,12 @@ fn default_reload_port() -> u32 {
|
||||
3001
|
||||
}
|
||||
|
||||
fn default_not_found_path() -> Arc<str> {
|
||||
"/404".into()
|
||||
fn default_not_found_path() -> String {
|
||||
"/404".to_string()
|
||||
}
|
||||
|
||||
fn default_hash_file_name() -> Arc<str> {
|
||||
"hash.txt".into()
|
||||
fn default_hash_file_name() -> String {
|
||||
"hash.txt".to_string()
|
||||
}
|
||||
|
||||
fn default_hash_files() -> bool {
|
||||
|
||||
@@ -30,14 +30,14 @@ fn ws_from_str_test() {
|
||||
|
||||
#[test]
|
||||
fn env_w_default_test() {
|
||||
temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
|
||||
_ = temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
|
||||
assert_eq!(
|
||||
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
|
||||
String::from("custom")
|
||||
);
|
||||
});
|
||||
|
||||
temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
|
||||
_ = temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
|
||||
assert_eq!(
|
||||
env_w_default("LEPTOS_CONFIG_ENV_TEST", "default").unwrap(),
|
||||
String::from("default")
|
||||
@@ -47,14 +47,14 @@ fn env_w_default_test() {
|
||||
|
||||
#[test]
|
||||
fn env_wo_default_test() {
|
||||
temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
|
||||
_ = temp_env::with_var("LEPTOS_CONFIG_ENV_TEST", Some("custom"), || {
|
||||
assert_eq!(
|
||||
env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(),
|
||||
Some(String::from("custom"))
|
||||
);
|
||||
});
|
||||
|
||||
temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
|
||||
_ = temp_env::with_var_unset("LEPTOS_CONFIG_ENV_TEST", || {
|
||||
assert_eq!(env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), None);
|
||||
});
|
||||
}
|
||||
@@ -76,9 +76,9 @@ fn try_from_env_test() {
|
||||
|| LeptosOptions::try_from_env().unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app_test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
|
||||
@@ -50,9 +50,9 @@ async fn get_configuration_from_file_ok() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -106,9 +106,9 @@ async fn get_config_from_file_ok() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -151,9 +151,9 @@ fn get_config_from_str_content() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -178,9 +178,9 @@ async fn get_config_from_env() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -202,8 +202,8 @@ async fn get_config_from_env() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.site_root.as_ref(), "target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "pkg");
|
||||
assert_eq!(config.site_root, "target/site");
|
||||
assert_eq!(config.site_pkg_dir, "pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
@@ -215,10 +215,10 @@ async fn get_config_from_env() {
|
||||
#[test]
|
||||
fn leptos_options_builder_default() {
|
||||
let conf = LeptosOptions::builder().output_name("app-test").build();
|
||||
assert_eq!(conf.output_name.as_ref(), "app-test");
|
||||
assert_eq!(conf.output_name, "app-test");
|
||||
assert!(matches!(conf.env, Env::DEV));
|
||||
assert_eq!(conf.site_pkg_dir.as_ref(), "pkg");
|
||||
assert_eq!(conf.site_root.as_ref(), ".");
|
||||
assert_eq!(conf.site_pkg_dir, "pkg");
|
||||
assert_eq!(conf.site_root, ".");
|
||||
assert_eq!(
|
||||
conf.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
@@ -242,9 +242,9 @@ fn environment_variable_override() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -265,9 +265,9 @@ fn environment_variable_override() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name.as_ref(), "app-test2");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site2");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg2");
|
||||
assert_eq!(config.output_name, "app-test2");
|
||||
assert_eq!(config.site_root, "my_target/site2");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg2");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:82").unwrap()
|
||||
|
||||
@@ -12,10 +12,10 @@ edition.workspace = true
|
||||
tachys = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.72"
|
||||
js-sys = "0.3.69"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen = "0.2.95"
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true }
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
version = "0.3.70"
|
||||
features = ["Location"]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,11 +4,11 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_error_panic_hook = "0.1.0"
|
||||
gloo = { version = "0.11.0", features = ["futures"] }
|
||||
leptos = { path = "../../../leptos", features = ["nightly", "csr", "tracing"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing = "0.1.0"
|
||||
tracing-subscriber = "0.3.0"
|
||||
tracing-subscriber-wasm = "0.1.0"
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -8,4 +8,5 @@ pub mod helpers;
|
||||
pub mod macro_helpers;
|
||||
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
#[macro_use]
|
||||
pub mod logging;
|
||||
|
||||
@@ -25,4 +25,4 @@ proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12.3"
|
||||
walkdir = "2.5"
|
||||
camino = "1.1"
|
||||
indexmap = "2.6"
|
||||
indexmap = "2.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.7.0-rc1"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -13,11 +13,11 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { version = "0.10.2", features = ["syn-full"] }
|
||||
attribute-derive = { version = "0.9.2", features = ["syn-full"] }
|
||||
cfg-if = "1.0"
|
||||
html-escape = "0.2.13"
|
||||
itertools = "0.13.0"
|
||||
prettyplease = "0.2.25"
|
||||
prettyplease = "0.2.20"
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
@@ -26,16 +26,16 @@ rstml = "0.12.0"
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4.22"
|
||||
typed-builder = "0.20.0"
|
||||
typed-builder = "0.19.1"
|
||||
trybuild = "1.0"
|
||||
leptos = { path = "../leptos" }
|
||||
server_fn = { path = "../server_fn", features = ["cbor"] }
|
||||
insta = "1.41"
|
||||
insta = "1.39"
|
||||
serde = "1.0"
|
||||
|
||||
[features]
|
||||
@@ -44,11 +44,10 @@ hydrate = []
|
||||
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = ["dep:tracing"]
|
||||
islands = []
|
||||
experimental-islands = []
|
||||
trace-component-props = []
|
||||
actix = ["server_fn_macro/actix"]
|
||||
axum = ["server_fn_macro/axum"]
|
||||
generic = ["server_fn_macro/generic"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
@@ -69,14 +68,6 @@ skip_feature_sets = [
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"generic",
|
||||
],
|
||||
[
|
||||
"generic",
|
||||
"axum",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Macros for use with the [`leptos`] framework.
|
||||
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
@@ -7,7 +5,6 @@
|
||||
// FIXME? every use of quote! {} is warning here -- false positive?
|
||||
#![allow(unknown_lints)]
|
||||
#![allow(private_macro_use)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error2;
|
||||
@@ -559,10 +556,10 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
/// Defines a component as an interactive island when you are using the
|
||||
/// `islands` feature of Leptos. Apart from the macro name,
|
||||
/// `experimental-islands` feature of Leptos. Apart from the macro name,
|
||||
/// the API is the same as the [`component`](macro@component) macro.
|
||||
///
|
||||
/// When you activate the `islands` feature, every `#[component]`
|
||||
/// When you activate the `experimental-islands` feature, every `#[component]`
|
||||
/// is server-only by default. This "default to server" behavior is important:
|
||||
/// you opt into shipping code to the client, rather than opting out. You can
|
||||
/// opt into client-side interactivity for any given component by changing from
|
||||
@@ -929,7 +926,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
|
||||
/// Derives a trait that parses a map of string keys and values into a typed
|
||||
/// data structure, e.g., for route params.
|
||||
#[proc_macro_derive(Params)]
|
||||
#[proc_macro_derive(Params, attributes(params))]
|
||||
pub fn params_derive(
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use super::{
|
||||
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
|
||||
};
|
||||
use super::{fragment_to_tokens, TagType};
|
||||
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
@@ -46,10 +44,9 @@ pub(crate) fn component_to_tokens(
|
||||
})
|
||||
.unwrap_or_else(|| node.attributes().len());
|
||||
|
||||
// Initially using uncloned mutable reference, as the node.key might be mutated during prop extraction (for nostrip:)
|
||||
let mut attrs = node
|
||||
.attributes_mut()
|
||||
.iter_mut()
|
||||
let attrs = node
|
||||
.attributes()
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
Some(node)
|
||||
@@ -57,46 +54,39 @@ pub(crate) fn component_to_tokens(
|
||||
None
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut required_props = vec![];
|
||||
let mut optional_props = vec![];
|
||||
for (_, attr) in attrs.iter_mut().enumerate().filter(|(idx, attr)| {
|
||||
idx < &spread_marker && {
|
||||
let attr_key = attr.key.to_string();
|
||||
!is_attr_let(&attr.key)
|
||||
&& !attr_key.starts_with("clone:")
|
||||
&& !attr_key.starts_with("class:")
|
||||
&& !attr_key.starts_with("style:")
|
||||
&& !attr_key.starts_with("attr:")
|
||||
&& !attr_key.starts_with("prop:")
|
||||
&& !attr_key.starts_with("on:")
|
||||
&& !attr_key.starts_with("use:")
|
||||
}
|
||||
}) {
|
||||
let optional = is_nostrip_optional_and_update_key(&mut attr.key);
|
||||
let name = &attr.key;
|
||||
let props = attrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, attr)| {
|
||||
idx < &spread_marker && {
|
||||
let attr_key = attr.key.to_string();
|
||||
!is_attr_let(&attr.key)
|
||||
&& !attr_key.starts_with("clone:")
|
||||
&& !attr_key.starts_with("class:")
|
||||
&& !attr_key.starts_with("style:")
|
||||
&& !attr_key.starts_with("attr:")
|
||||
&& !attr_key.starts_with("prop:")
|
||||
&& !attr_key.starts_with("on:")
|
||||
&& !attr_key.starts_with("use:")
|
||||
}
|
||||
})
|
||||
.map(|(_, attr)| {
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
|
||||
if optional {
|
||||
optional_props.push(quote! {
|
||||
props.#name = { #value }.map(Into::into);
|
||||
})
|
||||
} else {
|
||||
required_props.push(quote! {
|
||||
quote! {
|
||||
.#name(#[allow(unused_braces)] { #value })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the mutable reference to the node, go to an owned clone:
|
||||
let attrs = attrs.into_iter().map(|a| a.clone()).collect::<Vec<_>>();
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = attrs
|
||||
.iter()
|
||||
@@ -274,20 +264,14 @@ pub(crate) fn component_to_tokens(
|
||||
let mut component = quote! {
|
||||
{
|
||||
#[allow(unreachable_code)]
|
||||
#[allow(unused_mut)]
|
||||
#[allow(clippy::let_and_return)]
|
||||
::leptos::component::component_view(
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
&#name,
|
||||
{
|
||||
let mut props = ::leptos::component::component_props_builder(&#name #generics)
|
||||
#(#required_props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build();
|
||||
#(#optional_props)*
|
||||
props
|
||||
}
|
||||
::leptos::component::component_props_builder(&#name #generics)
|
||||
#(#props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build()
|
||||
)
|
||||
#spreads
|
||||
}
|
||||
|
||||
@@ -23,11 +23,8 @@ use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
};
|
||||
use syn::{
|
||||
punctuated::Pair::{End, Punctuated},
|
||||
spanned::Spanned,
|
||||
Expr,
|
||||
Expr::Tuple,
|
||||
ExprArray, ExprLit, ExprRange, Lit, LitStr, RangeLimits, Stmt,
|
||||
spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprRange, Lit, LitStr,
|
||||
RangeLimits, Stmt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -197,7 +194,7 @@ enum InertElementBuilder<'a> {
|
||||
},
|
||||
}
|
||||
|
||||
impl ToTokens for InertElementBuilder<'_> {
|
||||
impl<'a> ToTokens for InertElementBuilder<'a> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
InertElementBuilder::GlobalClass { strs, .. } => {
|
||||
@@ -219,7 +216,7 @@ enum GlobalClassItem<'a> {
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl ToTokens for GlobalClassItem<'_> {
|
||||
impl<'a> ToTokens for GlobalClassItem<'a> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let addl_tokens = match self {
|
||||
GlobalClassItem::Global(v) => v.to_token_stream(),
|
||||
@@ -653,9 +650,6 @@ pub(crate) fn element_to_tokens(
|
||||
_ => None,
|
||||
};
|
||||
match (key_a.as_deref(), key_b.as_deref()) {
|
||||
(Some("class"), Some("class")) | (Some("style"), Some("style")) => {
|
||||
Ordering::Equal
|
||||
}
|
||||
(Some("class"), _) | (Some("style"), _) => Ordering::Less,
|
||||
(_, Some("class")) | (_, Some("style")) => Ordering::Greater,
|
||||
_ => Ordering::Equal,
|
||||
@@ -667,18 +661,9 @@ pub(crate) fn element_to_tokens(
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
let mut name = attr.key.to_string();
|
||||
match tuple_name(&name, attr) {
|
||||
TupleName::None => {}
|
||||
TupleName::Str(tuple_name) => {
|
||||
name.push(':');
|
||||
name.push_str(&tuple_name);
|
||||
}
|
||||
TupleName::Array(names) => {
|
||||
for tuple_name in names {
|
||||
name.push(':');
|
||||
name.push_str(&tuple_name);
|
||||
}
|
||||
}
|
||||
if let Some(tuple_name) = tuple_name(&name, attr) {
|
||||
name.push(':');
|
||||
name.push_str(&tuple_name);
|
||||
}
|
||||
if names.contains(&name) {
|
||||
proc_macro_error2::emit_error!(
|
||||
@@ -1001,14 +986,10 @@ pub(crate) fn attribute_absolute(
|
||||
) -> Option<TokenStream> {
|
||||
let key = node.key.to_string();
|
||||
let contains_dash = key.contains('-');
|
||||
let attr_colon = key.starts_with("attr:")
|
||||
|| key.starts_with("style:")
|
||||
|| key.starts_with("class:")
|
||||
|| key.starts_with("prop:")
|
||||
|| key.starts_with("use:");
|
||||
let attr_aira = key.starts_with("attr:aria-");
|
||||
// anything that follows the x:y pattern
|
||||
match &node.key {
|
||||
NodeName::Punctuated(parts) if !contains_dash || attr_colon => {
|
||||
NodeName::Punctuated(parts) if !contains_dash || attr_aira => {
|
||||
if parts.len() >= 2 {
|
||||
let id = &parts[0];
|
||||
match id {
|
||||
@@ -1017,8 +998,7 @@ pub(crate) fn attribute_absolute(
|
||||
if id == "let" || id == "clone" {
|
||||
None
|
||||
} else if id == "attr" {
|
||||
let value = attribute_value(node, true);
|
||||
let multipart = parts.len() > 2;
|
||||
let value = attribute_value(node, true);
|
||||
let key = &parts[1];
|
||||
let key_name = key.to_string();
|
||||
if key_name == "class" || key_name == "style" {
|
||||
@@ -1034,15 +1014,6 @@ pub(crate) fn attribute_absolute(
|
||||
Some(
|
||||
quote! { ::leptos::tachys::html::attribute::#key(#value) },
|
||||
)
|
||||
} else if multipart {
|
||||
// e.g., attr:data-foo="bar"
|
||||
let key_name = parts.pairs().skip(1).map(|p| match p {
|
||||
Punctuated(n, p) => format!("{n}{p}"),
|
||||
End(n) => n.to_string(),
|
||||
}).collect::<String>();
|
||||
Some(
|
||||
quote! { ::leptos::tachys::html::attribute::custom::custom_attribute(#key_name, #value) },
|
||||
)
|
||||
} else {
|
||||
Some(
|
||||
quote! { ::leptos::tachys::html::attribute::#key(#value) },
|
||||
@@ -1212,32 +1183,6 @@ fn class_to_tokens(
|
||||
class: TokenStream,
|
||||
class_name: Option<&str>,
|
||||
) -> TokenStream {
|
||||
// case of class=(["foo", "bar"], /* something */)
|
||||
// just expands to multiple uses of class:
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let name = &tuple.elems[0];
|
||||
let value = &tuple.elems[1];
|
||||
if let Expr::Array(ExprArray { elems, .. }) = name {
|
||||
return elems
|
||||
.iter()
|
||||
.map(|elem| match elem {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) => quote! {
|
||||
.#class((#s, #value))
|
||||
},
|
||||
_ => proc_macro_error2::abort!(
|
||||
elem.span(),
|
||||
"invalid name"
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default case
|
||||
let value = attribute_value(node, false);
|
||||
if let Some(class_name) = class_name {
|
||||
quote! {
|
||||
@@ -1672,7 +1617,7 @@ pub(crate) fn directive_call_from_attribute_node(
|
||||
quote! { .directive(#handler, #[allow(clippy::useless_conversion)] #param) }
|
||||
}
|
||||
|
||||
fn tuple_name(name: &str, node: &KeyedAttribute) -> TupleName {
|
||||
fn tuple_name(name: &str, node: &KeyedAttribute) -> Option<String> {
|
||||
if name == "style" || name == "class" {
|
||||
if let Some(Tuple(tuple)) = node.value() {
|
||||
{
|
||||
@@ -1682,37 +1627,12 @@ fn tuple_name(name: &str, node: &KeyedAttribute) -> TupleName {
|
||||
lit: Lit::Str(s), ..
|
||||
}) = style_name
|
||||
{
|
||||
return TupleName::Str(s.value());
|
||||
} else if let Expr::Array(ExprArray { elems, .. }) =
|
||||
style_name
|
||||
{
|
||||
return TupleName::Array(
|
||||
elems
|
||||
.iter()
|
||||
.filter_map(|elem| match elem {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) => Some(s.value()),
|
||||
_ => proc_macro_error2::abort!(
|
||||
elem.span(),
|
||||
"invalid name"
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
return Some(s.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TupleName::None
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TupleName {
|
||||
None,
|
||||
Str(String),
|
||||
Array(Vec<String>),
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use proc_macro2::Ident;
|
||||
use quote::format_ident;
|
||||
use rstml::node::{KeyedAttribute, NodeName};
|
||||
use syn::{spanned::Spanned, ExprPath};
|
||||
use rstml::node::KeyedAttribute;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
pub fn filter_prefixed_attrs<'a, A>(attrs: A, prefix: &str) -> Vec<Ident>
|
||||
where
|
||||
@@ -17,37 +17,3 @@ where
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Handle nostrip: prefix:
|
||||
/// if there strip from the name, and return true to indicate that
|
||||
/// the prop should be an Option<T> and shouldn't be called on the builder if None,
|
||||
/// if Some(T) then T supplied to the builder.
|
||||
pub fn is_nostrip_optional_and_update_key(key: &mut NodeName) -> bool {
|
||||
let maybe_cleaned_name_and_span = if let NodeName::Punctuated(punct) = &key
|
||||
{
|
||||
if punct.len() == 2 {
|
||||
if let Some(cleaned_name) = key.to_string().strip_prefix("nostrip:")
|
||||
{
|
||||
punct
|
||||
.get(1)
|
||||
.map(|segment| (cleaned_name.to_string(), segment.span()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((cleaned_name, span)) = maybe_cleaned_name_and_span {
|
||||
*key = NodeName::Path(ExprPath {
|
||||
attrs: vec![],
|
||||
qself: None,
|
||||
path: format_ident!("{}", cleaned_name, span = span).into(),
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use leptos::prelude::*;
|
||||
#[component]
|
||||
fn Component(
|
||||
#[prop(optional)] optional: bool,
|
||||
#[prop(optional, into)] optional_into: Option<String>,
|
||||
#[prop(optional_no_strip)] optional_no_strip: Option<String>,
|
||||
#[prop(strip_option)] strip_option: Option<u8>,
|
||||
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
|
||||
@@ -12,7 +11,6 @@ fn Component(
|
||||
impl_trait: impl Fn() -> i32 + 'static,
|
||||
) -> impl IntoView {
|
||||
_ = optional;
|
||||
_ = optional_into;
|
||||
_ = optional_no_strip;
|
||||
_ = strip_option;
|
||||
_ = default;
|
||||
@@ -28,29 +26,9 @@ fn component() {
|
||||
.impl_trait(|| 42)
|
||||
.build();
|
||||
assert!(!cp.optional);
|
||||
assert_eq!(cp.optional_into, None);
|
||||
assert_eq!(cp.optional_no_strip, None);
|
||||
assert_eq!(cp.strip_option, Some(9));
|
||||
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
|
||||
assert_eq!(cp.into, "");
|
||||
assert_eq!((cp.impl_trait)(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_nostrip() {
|
||||
// Should compile (using nostrip:optional_into in second <Component />)
|
||||
view! {
|
||||
<Component
|
||||
optional_into="foo"
|
||||
strip_option=9
|
||||
into=""
|
||||
impl_trait=|| 42
|
||||
/>
|
||||
<Component
|
||||
nostrip:optional_into=Some("foo")
|
||||
strip_option=9
|
||||
into=""
|
||||
impl_trait=|| 42
|
||||
/>
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user