Compare commits

..

21 Commits

Author SHA1 Message Date
Luxalpa
2ef27cb0bb fix: URL encoding issue (closes #2602) (#2601) 2024-06-02 14:06:41 -04:00
SleeplessOne1917
21a6551ce6 feat: allow slice! macro to index tuples (#2598)
* Allow slice! macro to index tuples

* Undo changes to component tests

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-05-29 09:07:41 -04:00
Mingwei Samuel
2f4fd87c05 feat: #[component] now handles impl Trait by converting to generic type params, fix #2274 (#2599)
Book needs to be updated to remove this line:
35c380ffc8/src/view/03_components.md (L233)
2024-05-29 09:06:52 -04:00
Hecatron
13ad1b235d projects: example using the bevy 3d game engine and leptos (#2577)
* feat: Added example using the bevy 3d game engine and leptos

* fix: moved example to projects

* workspace fix
2024-05-27 15:55:27 -04:00
David Pitoniak
a2c7e23d54 docs: grammar typo for MultiActon doc comment (#2589) 2024-05-11 15:05:35 -04:00
Greg Johnston
9e65f71db4 fix: only issue NodeRef warning in debug mode (necessary to compile in --release) (#2587) 2024-05-11 15:05:17 -04:00
Luxalpa
7f4a2926c1 fix: StoredValue and Resource borrowMut error during dispose (#2583) 2024-05-11 15:04:57 -04:00
Hecatron
7c5203db19 examples: counter with DWARF debugging (breakpoints and sourcemap) (#2563)
* feat: Added initial dwarf debug counter example

* fix: update to readme and launch.json, task.json

* fix: fix tasks.json for debugging

* fix: added Trunk.toml to fix the port

* fix: moved example to projects
2024-05-11 15:02:33 -04:00
Greg Johnston
3760ced0ec fix: allow temporaries as props (closes #2541) (#2582) 2024-05-08 19:35:57 -04:00
Greg Johnston
f3f3a053ba fix: don't insert empty child for comment/doctype (closes #2549) (#2581) 2024-05-08 07:19:57 -04:00
Antoine Büsch
6a8e4bb453 Fix empty_docs warnings in #[component] macro (#2574) 2024-05-06 22:09:19 -04:00
Luxalpa
20f4323e50 feat: allow customize derives for serverfn input struct (#2545) 2024-05-06 08:54:29 -04:00
martin frances
47bcee0ef4 docs: improve NodeRef warning (#2414) (#2467) 2024-05-06 08:51:32 -04:00
SleeplessOne1917
ac3b95d35a examples: use trunk's built-in way of handling tailwind (#2557)
* Use trunk built-in way of handling tailwind

* Remove package manager from package.json
2024-05-06 08:49:07 -04:00
Greg Johnston
a314a4fcd9 docs: clarify the purpose of local resources (#2543) 2024-05-06 08:48:29 -04:00
Sam Judelson
b2a77f06b9 projects: OpenAPI Utopia (#2556) 2024-05-06 08:48:09 -04:00
Sam Judelson
9741c41356 projects: added an index to projects README (#2555)
The Index gives a high level overview of the projects
2024-05-06 08:47:13 -04:00
Joey McKenzie
4e4a770600 projects: add sitemap demo project (#2553) 2024-05-06 08:46:49 -04:00
martin frances
289c02fdac Minor: examples/server_fns_axum FileWatcher logs errors to the console. (#2547)
* Minor: examples/server_fns_axum FileWatcher logs errors to the console.

The cause is an assumption that the directory

./watched_files/

exits.

* chore: Now using .gitkeep to preserve directory structure.
2024-05-06 08:45:27 -04:00
itowlson
123d95c34c Update leptos-spin-macro reference (#2570)
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
2024-05-02 15:25:22 -07:00
Greg Johnston
da9711a743 docs: add caveats for ProtectedRoute (#2558) 2024-05-01 07:06:54 -04:00
101 changed files with 3037 additions and 2063 deletions

View File

@@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = [
# utilities
# utilities
"oco",
# core

View File

@@ -47,11 +47,11 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
workspace = false
description = "Generate the list of workspace members"
script = '''
examples=$(ls |
grep -v .md |
grep -v Makefile.toml |
grep -v cargo-make |
grep -v gtk |
examples=$(ls |
grep -v .md |
grep -v Makefile.toml |
grep -v cargo-make |
grep -v gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''

View File

@@ -8,7 +8,7 @@ leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
leptos_router = { path = "../../router", features = ["csr"] }
log = "0.4"
gloo-net = { version = "0.2", features = ["http"] }
gloo-net = { version = "0.5", features = ["http"] }
# dependencies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2" }

View File

@@ -1,4 +1,4 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]

View File

@@ -1,4 +0,0 @@
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]

View File

@@ -1,14 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<head>
<meta charset="utf-8" />
<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/output.css" />
<link data-trunk rel="tailwind-css" href="/style/tailwind.css" />
<title>Leptos • Counter with Tailwind</title>
</head>
<body></body>
</head>
<body></body>
</html>

View File

@@ -1,604 +0,0 @@
/*
! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.max-w-3xl {
max-width: 48rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-amber-600 {
--tw-bg-opacity: 1;
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

View File

@@ -16,7 +16,7 @@ leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
leptos-spin-macro = { version = "0.1", optional = true }
leptos-spin-macro = { version = "0.2", optional = true }
tracing = "0.1"
typed-builder = "0.18"
typed-builder-macro = "0.18"

View File

@@ -31,10 +31,12 @@ use std::cell::Cell;
/// }
/// }
/// ```
#[repr(transparent)]
pub struct NodeRef<T: ElementDescriptor + 'static>(
RwSignal<Option<HtmlElement<T>>>,
);
#[cfg_attr(not(debug_assertions), repr(transparent))]
pub struct NodeRef<T: ElementDescriptor + 'static> {
element: RwSignal<Option<HtmlElement<T>>>,
#[cfg(debug_assertions)]
defined_at: &'static std::panic::Location<'static>,
}
/// Creates a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
@@ -65,9 +67,14 @@ pub struct NodeRef<T: ElementDescriptor + 'static>(
/// }
/// }
/// ```
#[track_caller]
#[inline(always)]
pub fn create_node_ref<T: ElementDescriptor + 'static>() -> NodeRef<T> {
NodeRef(create_rw_signal(None))
NodeRef {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
element: create_rw_signal(None),
}
}
impl<T: ElementDescriptor + 'static> NodeRef<T> {
@@ -120,7 +127,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
where
T: Clone,
{
self.0.get()
self.element.get()
}
/// Gets the element that is currently stored in the reference.
@@ -132,7 +139,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
where
T: Clone,
{
self.0.get_untracked()
self.element.get_untracked()
}
#[doc(hidden)]
@@ -144,13 +151,15 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
where
T: Clone,
{
self.0.update(|current| {
self.element.update(|current| {
if current.is_some() {
#[cfg(debug_assertions)]
crate::debug_warn!(
"You are setting a NodeRef that has already been filled. \
Its possible this is intentional, but its also \
possible that youre accidentally using the same NodeRef \
for multiple _ref attributes."
"You are setting the NodeRef defined at {}, which has \
already been filled Its possible this is intentional, \
but its also possible that youre accidentally using \
the same NodeRef for multiple _ref attributes.",
self.defined_at
);
}
*current = Some(node.clone());

View File

@@ -8,10 +8,11 @@ use leptos_hot_reload::parsing::value_to_string;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, spanned::Spanned,
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType,
Signature, Stmt, Type, TypePath, Visibility,
parse::Parse, parse_quote, spanned::Spanned, token::Colon,
visit_mut::VisitMut, AngleBracketedGenericArguments, Attribute, FnArg,
GenericArgument, GenericParam, Item, ItemFn, LitStr, Meta, Pat, PatIdent,
Path, PathArguments, ReturnType, Signature, Stmt, Type, TypeImplTrait,
TypeParam, TypePath, Visibility,
};
pub struct Model {
@@ -28,6 +29,7 @@ pub struct Model {
impl Parse for Model {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut item = ItemFn::parse(input)?;
convert_impl_trait_to_generic(&mut item.sig);
let docs = Docs::new(&item.attrs);
@@ -178,6 +180,18 @@ impl ToTokens for Model {
);
let component_fn_prop_docs = generate_component_fn_prop_docs(props);
let docs_and_prop_docs = if component_fn_prop_docs.is_empty() {
// Avoid generating an empty doc line in case the component has no doc and no props.
quote! {
#docs
}
} else {
quote! {
#docs
#[doc = ""]
#component_fn_prop_docs
}
};
let (
tracing_instrument_attr,
@@ -502,9 +516,7 @@ impl ToTokens for Model {
let output = quote! {
#[doc = #builder_name_doc]
#[doc = ""]
#docs
#[doc = ""]
#component_fn_prop_docs
#docs_and_prop_docs
#[derive(::leptos::typed_builder_macro::TypedBuilder #props_derive_serialize)]
//#[builder(doc)]
#[builder(crate_module_path=::leptos::typed_builder)]
@@ -548,9 +560,7 @@ impl ToTokens for Model {
#into_view
#docs
#[doc = ""]
#component_fn_prop_docs
#docs_and_prop_docs
#[allow(non_snake_case, clippy::too_many_arguments)]
#[allow(clippy::needless_lifetimes)]
#tracing_instrument_attr
@@ -1221,3 +1231,57 @@ fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident {
Ident::new(&format!("__{ident}"), ident.span())
}
/// Converts all `impl Trait`s in a function signature to use generic params instead.
fn convert_impl_trait_to_generic(sig: &mut Signature) {
fn new_generic_ident(i: usize, span: Span) -> Ident {
Ident::new(&format!("__ImplTrait{}", i), span)
}
// First: visit all `impl Trait`s and replace them with new generic params.
#[derive(Default)]
struct RemoveImplTrait(Vec<TypeImplTrait>);
impl VisitMut for RemoveImplTrait {
fn visit_type_mut(&mut self, ty: &mut Type) {
syn::visit_mut::visit_type_mut(self, ty);
if matches!(ty, Type::ImplTrait(_)) {
let ident = new_generic_ident(self.0.len(), ty.span());
let generic_type = Type::Path(TypePath {
qself: None,
path: Path::from(ident),
});
let Type::ImplTrait(impl_trait) =
std::mem::replace(ty, generic_type)
else {
unreachable!();
};
self.0.push(impl_trait);
}
}
// Early exits.
fn visit_attribute_mut(&mut self, _: &mut Attribute) {}
fn visit_pat_mut(&mut self, _: &mut Pat) {}
}
let mut visitor = RemoveImplTrait::default();
for fn_arg in sig.inputs.iter_mut() {
visitor.visit_fn_arg_mut(fn_arg);
}
let RemoveImplTrait(impl_traits) = visitor;
// Second: Add the new generic params into the signature.
for (i, impl_trait) in impl_traits.into_iter().enumerate() {
let span = impl_trait.span();
let ident = new_generic_ident(i, span);
// We can simply append to the end (only lifetime params must be first).
// Note currently default generics are not allowed in `fn`, so not a concern.
sig.generics.params.push(GenericParam::Type(TypeParam {
attrs: vec![],
ident,
colon_token: Some(Colon { spans: [span] }),
bounds: impl_trait.bounds,
eq_token: None,
default: None,
}));
}
}

View File

@@ -11,7 +11,7 @@ use syn::{
struct SliceMacroInput {
root: syn::Ident,
path: Punctuated<syn::Ident, Token![.]>,
path: Punctuated<syn::Member, Token![.]>,
}
impl Parse for SliceMacroInput {
@@ -19,7 +19,7 @@ impl Parse for SliceMacroInput {
let root: syn::Ident = input.parse()?;
input.parse::<Token![.]>()?;
// do not accept trailing punctuation
let path: Punctuated<syn::Ident, Token![.]> =
let path: Punctuated<syn::Member, Token![.]> =
Punctuated::parse_separated_nonempty(input)?;
if path.is_empty() {

View File

@@ -298,34 +298,38 @@ pub(crate) fn element_to_tokens(
let children = node
.children
.iter()
.map(|node| match node {
Node::Fragment(fragment) => fragment_to_tokens(
&fragment.children,
true,
parent_type,
None,
global_class,
None,
)
.unwrap_or(quote_spanned! {
Span::call_site()=> ::leptos::leptos_dom::Unit
}),
Node::Text(node) => quote! { #node },
.filter_map(|node| match node {
Node::Fragment(fragment) => Some(
fragment_to_tokens(
&fragment.children,
true,
parent_type,
None,
global_class,
None,
)
.unwrap_or(quote_spanned! {
Span::call_site()=> ::leptos::leptos_dom::Unit
}),
),
Node::Text(node) => Some(quote! { #node }),
Node::RawText(node) => {
let text = node.to_string_best();
let text = syn::LitStr::new(&text, node.span());
quote! { #text }
Some(quote! { #text })
}
Node::Block(node) => quote! { #node },
Node::Element(node) => element_to_tokens(
node,
parent_type,
None,
global_class,
None,
)
.unwrap_or_default(),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Block(node) => Some(quote! { #node }),
Node::Element(node) => Some(
element_to_tokens(
node,
parent_type,
None,
global_class,
None,
)
.unwrap_or_default(),
),
Node::Comment(_) | Node::Doctype(_) => None,
})
.map(|node| quote!(.child(#node)));

View File

@@ -237,25 +237,17 @@ pub(crate) fn component_to_tokens(
#[allow(unused_mut)] // used in debug
let mut component = quote_spanned! {node.span()=>
{
let props = #component_props_builder
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
#name_ref,
#component_props_builder
#(#props)*
#(#slots)*
#children;
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props
#children
#build
#dyn_attrs
#(#spread_bindings)*;
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
#name_ref,
props
)
}
#(#spread_bindings)*
)
};
// (Temporarily?) removed

View File

@@ -1,21 +1,14 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
{
let props = ::leptos::component_props_builder(&SimpleCounter)
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
::leptos::component_props_builder(&SimpleCounter)
.initial_value(#[allow(unused_braces)] { 0 })
.step(#[allow(unused_braces)] { 1 });
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props.build();
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
props,
)
}
.step(#[allow(unused_braces)] { 1 })
.build(),
)
}

View File

@@ -1,23 +1,16 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
::leptos::IntoView::into_view(
#[allow(unused_braces)]
{
{
let props = ::leptos::component_props_builder(&ExternalComponent);
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props.build();
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
props,
)
}
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
},
)
.on(
@@ -27,4 +20,3 @@ fn view() {
move |_: Event| set_value(0),
)
}

View File

@@ -1,22 +1,89 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Brace,
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: let,
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '=',
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
@@ -163,90 +230,6 @@ TokenStream [
],
span: bytes(65..71),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: let_unit_value,
span: bytes(10..82),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: unit_arg,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Ident {
sym: let,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '=',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
@@ -261,127 +244,6 @@ TokenStream [
stream: TokenStream [],
span: bytes(11..24),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unreachable_code,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},

View File

@@ -1,6 +1,5 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
@@ -77,19 +76,87 @@ TokenStream [
Group {
delimiter: Brace,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Brace,
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: let,
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '=',
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
@@ -136,90 +203,6 @@ TokenStream [
],
span: bytes(11..28),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: let_unit_value,
span: bytes(10..82),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: unit_arg,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Ident {
sym: let,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '=',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
@@ -234,127 +217,6 @@ TokenStream [
stream: TokenStream [],
span: bytes(11..28),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unreachable_code,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},

View File

@@ -1,21 +1,14 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
{
let props = ::leptos::component_props_builder(&SimpleCounter)
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
::leptos::component_props_builder(&SimpleCounter)
.initial_value(#[allow(unused_braces)] { 0 })
.step(#[allow(unused_braces)] { 1 });
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props.build();
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
props,
)
}
.step(#[allow(unused_braces)] { 1 })
.build(),
)
}

View File

@@ -1,23 +1,16 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
::leptos::IntoView::into_view(
#[allow(unused_braces)]
{
{
let props = ::leptos::component_props_builder(&ExternalComponent);
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props.build();
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
props,
)
}
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
},
)
.on(
@@ -27,4 +20,3 @@ fn view() {
move |_: Event| set_value(0),
)
}

View File

@@ -1,22 +1,89 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Brace,
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: let,
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '=',
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
@@ -163,90 +230,6 @@ TokenStream [
],
span: bytes(65..71),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: let_unit_value,
span: bytes(10..82),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: unit_arg,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Ident {
sym: let,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '=',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
@@ -261,127 +244,6 @@ TokenStream [
stream: TokenStream [],
span: bytes(11..24),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unreachable_code,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},

View File

@@ -1,6 +1,5 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
@@ -77,19 +76,87 @@ TokenStream [
Group {
delimiter: Brace,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Brace,
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: let,
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '=',
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
@@ -136,90 +203,6 @@ TokenStream [
],
span: bytes(11..28),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: let_unit_value,
span: bytes(10..82),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: unit_arg,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Ident {
sym: let,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '=',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
@@ -234,127 +217,6 @@ TokenStream [
stream: TokenStream [],
span: bytes(11..28),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unreachable_code,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},

View File

@@ -1,21 +1,14 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
{
let props = ::leptos::component_props_builder(&SimpleCounter)
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
::leptos::component_props_builder(&SimpleCounter)
.initial_value(#[allow(unused_braces)] { 0 })
.step(#[allow(unused_braces)] { 1 });
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props.build();
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
props,
)
}
.step(#[allow(unused_braces)] { 1 })
.build(),
)
}

View File

@@ -1,23 +1,16 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
::leptos::IntoView::into_view(
#[allow(unused_braces)]
{
{
let props = ::leptos::component_props_builder(&ExternalComponent);
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props.build();
#[allow(unreachable_code)]
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
props,
)
}
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
},
)
.on(
@@ -27,4 +20,3 @@ fn view() {
move |_: Event| set_value(0),
)
}

View File

@@ -1,22 +1,89 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Brace,
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: let,
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '=',
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
@@ -163,90 +230,6 @@ TokenStream [
],
span: bytes(65..71),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: let_unit_value,
span: bytes(10..82),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: unit_arg,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Ident {
sym: let,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '=',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
@@ -261,127 +244,6 @@ TokenStream [
stream: TokenStream [],
span: bytes(11..24),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unreachable_code,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},

View File

@@ -1,6 +1,5 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: result
---
TokenStream [
@@ -77,19 +76,87 @@ TokenStream [
Group {
delimiter: Brace,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Brace,
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: let,
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '=',
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
@@ -136,90 +203,6 @@ TokenStream [
],
span: bytes(11..28),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: let_unit_value,
span: bytes(10..82),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: unit_arg,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Ident {
sym: let,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '=',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
@@ -234,127 +217,6 @@ TokenStream [
stream: TokenStream [],
span: bytes(11..28),
},
Punct {
char: ';',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unreachable_code,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: props,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},

View File

@@ -8,20 +8,27 @@ fn Component(
#[prop(strip_option)] strip_option: Option<u8>,
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
#[prop(into)] into: String,
impl_trait: impl Fn() -> i32 + 'static,
) -> impl IntoView {
_ = optional;
_ = optional_no_strip;
_ = strip_option;
_ = default;
_ = into;
_ = impl_trait;
}
#[test]
fn component() {
let cp = ComponentProps::builder().into("").strip_option(9).build();
let cp = ComponentProps::builder()
.into("")
.strip_option(9)
.impl_trait(|| 42)
.build();
assert!(!cp.optional);
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);
}

View File

@@ -10,9 +10,12 @@ pub struct OuterState {
#[derive(Clone, PartialEq, Default)]
pub struct InnerState {
inner_count: i32,
inner_name: String,
inner_tuple: InnerTuple,
}
#[derive(Clone, PartialEq, Default)]
pub struct InnerTuple(String);
#[test]
fn green() {
let _ = create_runtime();
@@ -22,7 +25,7 @@ fn green() {
let (_, _) = slice!(outer_signal.count);
let (_, _) = slice!(outer_signal.inner.inner_count);
let (_, _) = slice!(outer_signal.inner.inner_name);
let (_, _) = slice!(outer_signal.inner.inner_tuple.0);
}
#[test]

View File

@@ -14,7 +14,7 @@ error: expected `.`
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected identifier
error: unexpected end of input, expected identifier or integer
--> tests/slice/red.rs:25:18
|
25 | let (_, _) = slice!(outer_signal.);
@@ -22,7 +22,7 @@ error: unexpected end of input, expected identifier
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected identifier
error: unexpected end of input, expected identifier or integer
--> tests/slice/red.rs:27:18
|
27 | let (_, _) = slice!(outer_signal.inner.);

View File

@@ -271,6 +271,16 @@ where
///
/// Local resources do not load on the server, only in the clients browser.
///
/// ## When to use a Local Resource
///
/// `create_resource` has three different features:
/// 1. gives a synchronous API for asynchronous things
/// 2. integrates with `Suspense`/`Transition``
/// 3. makes your application faster by starting things like DB access or an API request on the server,
/// rather than waiting until you've fully loaded the client
///
/// `create_local_resource` is useful when you can't or don't need to do #3 (serializing data from server
/// to client), but still want #1 (synchronous API for async) and #2 (integration with `Suspense`).
/// ```
/// # use leptos_reactive::*;
/// # let runtime = create_runtime();
@@ -278,7 +288,9 @@ where
/// struct ComplicatedUnserializableStruct {
/// // something here that can't be serialized
/// }
/// // any old async function; maybe this is calling a REST API or something
///
/// // an async function whose results can't be serialized from the server to the client
/// // (for example, opening a connection to the user's device camera)
/// async fn setup_complicated_struct() -> ComplicatedUnserializableStruct {
/// // do some work
/// ComplicatedUnserializableStruct {}

View File

@@ -294,10 +294,12 @@ impl Runtime {
drop(node);
}
ScopeProperty::Resource(id) => {
self.resources.borrow_mut().remove(id);
let value = self.resources.borrow_mut().remove(id);
drop(value);
}
ScopeProperty::StoredValue(id) => {
self.stored_values.borrow_mut().remove(id);
let value = self.stored_values.borrow_mut().remove(id);
drop(value);
}
}
}

View File

@@ -242,7 +242,7 @@ where
}
}
/// Creates an [MultiAction] to synchronize an imperative `async` call to the synchronous reactive system.
/// Creates a [MultiAction] to synchronize an imperative `async` call to the synchronous reactive system.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [create_resource](leptos_reactive::create_resource) instead. If youre trying
@@ -319,7 +319,7 @@ where
}))
}
/// Creates an [MultiAction] that can be used to call a server function.
/// Creates a [MultiAction] that can be used to call a server function.
///
/// ```rust,ignore
/// # use leptos::*;

View File

@@ -5,3 +5,24 @@ The `projects` directory is intended as a collective of medium-to-large-scale ex
The `examples` directory is included in our CI, and examples are regularly linted and tested. The barrier to entry for the `projects` directory is intended to be lower: Example projects will generally be built against a particular version, and not regularly linted or updated. Hopefully this distinction allows us to accept more examples without worrying about the maintenance burden of constant updates.
Feel free to submit projects to this directory via PR!
## Index
### meilisearch-searchbar
[Meilisearch](https://www.meilisearch.com/) is a search engine built in Rust that you can self-host. This example shows how to run it alongside a leptos server and present a search bar with autocomplete to the user.
### nginx-mpmc
[Nginx](https://nginx.org/) Multiple Producer Multi Consumer, this example shows how you can use Nginx to provide different clients to the user while running multiple Leptos servers that provide server functions to any of the clients.
### ory-kratos
[Ory](https://www.ory.sh/docs/welcome) is a combination of different authorization services. Ory Kratos is their Identification service, which provides password storage, emailing, login and registration functionality, etc. This example shows running Ory Kratos alongside a leptos server and making use of their UI Node data types in leptos. TODO: This example needs a bit more work to show off SSO passwordless etc
### tauri-from-scratch
This example walks you through in explicit detail how to use [Tauri](https://tauri.app/) to render your Leptos App on non web targets using [WebView](https://en.wikipedia.org/wiki/WebView) while communicating with your leptos server and servering an SSR supported web experience. TODO: It could be simplified since part of the readme includes copying and pasting boilerplate.
### counter_dwarf_debug
This example shows how to add breakpoints within the browser or visual studio code for debugging.
### bevy3d_ui
This example uses the bevy 3d game engine with leptos within webassembly.

View File

@@ -0,0 +1,26 @@
[package]
name = "bevy3d_ui"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { version = "0.6.11", features = ["csr"] }
leptos_meta = { version = "0.6.11", features = ["csr"] }
leptos_router = { version = "0.6.11", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
bevy = "0.13.2"
crossbeam-channel = "0.5.12"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"
[workspace]
# The empty workspace here is to keep rust-analyzer satisfied

View File

@@ -0,0 +1,15 @@
# Bevy 3D UI Example
This example combines a leptos UI with a bevy 3D view.
Bevy is a 3D game engine written in rust that can be compiled to web assembly by using the wgpu library.
The wgpu library in turn can target the newer webgpu standard or the older webgl for web browsers.
In the case of a desktop application, if you wanted to use a styled ui via leptos and a 3d view via bevy
you could also combine this with tauri.
## Quick Start
* Run `trunk serve to run the example.
* Browse to http://127.0.0.1:8080/
It's best to use a web browser with webgpu capability for best results such as Chrome or Opera.

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View File

@@ -0,0 +1,38 @@
use bevy::prelude::*;
/// Event Processor
#[derive(Resource)]
pub struct EventProcessor<TSender, TReceiver> {
pub sender: crossbeam_channel::Sender<TSender>,
pub receiver: crossbeam_channel::Receiver<TReceiver>,
}
impl<TSender, TReceiver> Clone for EventProcessor<TSender, TReceiver> {
fn clone(&self) -> Self {
Self {
sender: self.sender.clone(),
receiver: self.receiver.clone(),
}
}
}
/// Events sent from the client to bevy
#[derive(Debug)]
pub enum ClientInEvents {
/// Update the 3d model position from the client
CounterEvt(CounterEvtData),
}
/// Events sent out from bevy to the client
#[derive(Debug)]
pub enum PluginOutEvents {
/// TODO Feed back to the client an event from bevy
Click,
}
/// Input event to update the bevy view from the client
#[derive(Clone, Debug, Event)]
pub struct CounterEvtData {
/// Amount to move on the Y Axis
pub value: f32,
}

View File

@@ -0,0 +1,2 @@
pub mod events;
pub mod plugin;

View File

@@ -0,0 +1,63 @@
use super::events::*;
use bevy::prelude::*;
/// Events plugin for bevy
#[derive(Clone)]
pub struct DuplexEventsPlugin {
/// Client processor for sending ClientInEvents, receiving PluginOutEvents
client_processor: EventProcessor<ClientInEvents, PluginOutEvents>,
/// Internal processor for sending PluginOutEvents, receiving ClientInEvents
plugin_processor: EventProcessor<PluginOutEvents, ClientInEvents>,
}
impl DuplexEventsPlugin {
/// Create a new instance
pub fn new() -> DuplexEventsPlugin {
// For sending messages from bevy to the client
let (bevy_sender, client_receiver) = crossbeam_channel::bounded(50);
// For sending message from the client to bevy
let (client_sender, bevy_receiver) = crossbeam_channel::bounded(50);
let instance = DuplexEventsPlugin {
client_processor: EventProcessor {
sender: client_sender,
receiver: client_receiver,
},
plugin_processor: EventProcessor {
sender: bevy_sender,
receiver: bevy_receiver,
},
};
instance
}
/// Get the client event processor
pub fn get_processor(
&self,
) -> EventProcessor<ClientInEvents, PluginOutEvents> {
self.client_processor.clone()
}
}
/// Build the bevy plugin and attach
impl Plugin for DuplexEventsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(self.plugin_processor.clone())
.init_resource::<Events<CounterEvtData>>()
.add_systems(PreUpdate, input_events_system);
}
}
/// Send the event to bevy using EventWriter
fn input_events_system(
int_processor: Res<EventProcessor<PluginOutEvents, ClientInEvents>>,
mut counter_event_writer: EventWriter<CounterEvtData>,
) {
for input_event in int_processor.receiver.try_iter() {
match input_event {
ClientInEvents::CounterEvt(event) => {
// Send event through Bevy's event system
counter_event_writer.send(event);
}
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod eventqueue;
pub mod scene;
pub mod state;

View File

@@ -0,0 +1,124 @@
use super::eventqueue::events::{
ClientInEvents, CounterEvtData, EventProcessor, PluginOutEvents,
};
use super::eventqueue::plugin::DuplexEventsPlugin;
use super::state::{Shared, SharedResource, SharedState};
use bevy::prelude::*;
/// Represents the Cube in the scene
#[derive(Component, Copy, Clone)]
pub struct Cube;
/// Represents the 3D Scene
#[derive(Clone)]
pub struct Scene {
is_setup: bool,
canvas_id: String,
evt_plugin: DuplexEventsPlugin,
shared_state: Shared<SharedState>,
processor: EventProcessor<ClientInEvents, PluginOutEvents>,
}
impl Scene {
/// Create a new instance
pub fn new(canvas_id: String) -> Scene {
let plugin = DuplexEventsPlugin::new();
let instance = Scene {
is_setup: false,
canvas_id: canvas_id,
evt_plugin: plugin.clone(),
shared_state: SharedState::new(),
processor: plugin.get_processor(),
};
instance
}
/// Get the shared state
pub fn get_state(&self) -> Shared<SharedState> {
self.shared_state.clone()
}
/// Get the event processor
pub fn get_processor(
&self,
) -> EventProcessor<ClientInEvents, PluginOutEvents> {
self.processor.clone()
}
/// Setup and attach the bevy instance to the html canvas element
pub fn setup(&mut self) {
if self.is_setup == true {
return;
};
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some(self.canvas_id.clone()),
..default()
}),
..default()
}))
.add_plugins(self.evt_plugin.clone())
.insert_resource(SharedResource(self.shared_state.clone()))
.add_systems(Startup, setup_scene)
.add_systems(Update, handle_bevy_event)
.run();
self.is_setup = true;
}
}
/// Setup the scene
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
resource: Res<SharedResource>,
) {
let name = resource.0.lock().unwrap().name.clone();
// circular base
commands.spawn(PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(
-std::f32::consts::FRAC_PI_2,
)),
..default()
});
// cube
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::rgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
Cube,
));
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn(TextBundle::from_section(name, TextStyle::default()));
}
/// Move the Cube on event
fn handle_bevy_event(
mut counter_event_reader: EventReader<CounterEvtData>,
mut cube_query: Query<&mut Transform, With<Cube>>,
) {
let mut cube_transform = cube_query.get_single_mut().expect("no cube :(");
for _ev in counter_event_reader.read() {
cube_transform.translation += Vec3::new(0.0, _ev.value, 0.0);
}
}

View File

@@ -0,0 +1,24 @@
use bevy::ecs::system::Resource;
use std::sync::{Arc, Mutex};
pub type Shared<T> = Arc<Mutex<T>>;
/// Shared Resource used for Bevy
#[derive(Resource)]
pub struct SharedResource(pub Shared<SharedState>);
/// Shared State
pub struct SharedState {
pub name: String,
}
impl SharedState {
/// Get a new shared state
pub fn new() -> Arc<Mutex<SharedState>> {
let state = SharedState {
name: "This can be used for shared state".to_string(),
};
let shared = Arc::new(Mutex::new(state));
shared
}
}

View File

@@ -0,0 +1 @@
pub mod bevydemo1;

View File

@@ -0,0 +1,11 @@
mod demos;
mod routes;
use leptos::*;
use routes::RootPage;
pub fn main() {
// Bevy will output a lot of debug info to the console when this is enabled.
//_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| view! { <RootPage/> })
}

View File

@@ -0,0 +1,52 @@
use crate::demos::bevydemo1::eventqueue::events::{
ClientInEvents, CounterEvtData,
};
use crate::demos::bevydemo1::scene::Scene;
use leptos::*;
/// 3d view component
#[component]
pub fn Demo1() -> impl IntoView {
// Setup a Counter
let initial_value: i32 = 0;
let step: i32 = 1;
let (value, set_value) = create_signal(initial_value);
// Setup a bevy 3d scene
let scene = Scene::new("#bevy".to_string());
let sender = scene.get_processor().sender;
let (sender_sig, _set_sender_sig) = create_signal(sender);
let (scene_sig, _set_scene_sig) = create_signal(scene);
// We need to add the 3D view onto the canvas post render.
create_effect(move |_| {
request_animation_frame(move || {
scene_sig.get().setup();
});
});
view! {
<div>
<button on:click=move |_| set_value.set(0)>"Clear"</button>
<button on:click=move |_| {
set_value.update(|value| *value -= step);
let newpos = (step as f32) / 10.0;
sender_sig
.get()
.send(ClientInEvents::CounterEvt(CounterEvtData { value: -newpos }))
.expect("could not send event");
}>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| {
set_value.update(|value| *value += step);
let newpos = step as f32 / 10.0;
sender_sig
.get()
.send(ClientInEvents::CounterEvt(CounterEvtData { value: newpos }))
.expect("could not send event");
}>"+1"</button>
</div>
<canvas id="bevy" width="800" height="600"></canvas>
}
}

View File

@@ -0,0 +1,24 @@
pub mod demo1;
use demo1::Demo1;
use leptos::*;
use leptos_meta::{provide_meta_context, Meta, Stylesheet, Title};
use leptos_router::*;
#[component]
pub fn RootPage() -> impl IntoView {
provide_meta_context();
view! {
<Meta name="charset" content="UTF-8"/>
<Meta name="description" content="Leptonic CSR template"/>
<Meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<Meta name="theme-color" content="#e66956"/>
<Stylesheet href="https://fonts.googleapis.com/css?family=Roboto&display=swap"/>
<Title text="Leptos Bevy3D Example"/>
<Router>
<Routes>
<Route path="" view=|| view! { <Demo1/> }/>
</Routes>
</Router>
}
}

View File

@@ -0,0 +1,2 @@
# For this example we want to include the vscode files
!.vscode

View File

@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Browser Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:4001",
"webRoot": "${workspaceFolder}/dist",
// Needed to keep the dwarf extension in the browser
"userDataDir": false,
"preLaunchTask": "trunk: serve",
"postDebugTask": "postdebugKill"
},
]
}

View File

@@ -0,0 +1,53 @@
{
"version": "2.0.0",
"tasks": [
// Task to build the sources
{
"label": "trunk: build",
"type": "shell",
"command": "trunk",
"args": ["build"],
"problemMatcher": [
"$rustc"
],
"group": "build",
},
// Task to launch trunk serve for debugging
{
"label": "trunk: serve",
"type": "shell",
"command": "trunk",
"args": ["serve"],
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": ".",
"file": 1,"line": 1,
"column": 1,"message": 1
},
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
},
// Terminate the trunk serve task
{
"label": "postdebugKill",
"type": "shell",
"command": "echo ${input:terminate}",
},
],
"inputs": [
{
"id": "terminate",
"type": "command",
"command": "workbench.action.tasks.terminate",
"args": "terminateAll"
}
]
}

View File

@@ -0,0 +1,22 @@
[workspace]
# The empty workspace here is to keep rust-analyzer satisfied
[package]
name = "counter_dwarf_debug"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"

View File

@@ -0,0 +1,74 @@
# Debugging Leptos Counter Example in Browser and VSCode
This example builds on the simple counter by adding breakpoints and single stepping the source code for debugging.
Both within the browser and VSCode.
This uses a new feature of wasm called Dwarf which is a form of source code mapping.
Note variable inspection during the breakpoints doesn't seem to work at this stage.
## Quick Start
* Install the requirements below
* Open this directory within visual studio code
* Add a breakpoint to the code
* Launch the example using the visual studio code debug launcher
## How This Works
### Html Changes
First we need to make a change to the index.html file
From this
```html
<link data-trunk rel="rust" data-wasm-opt="z"/>
```
To this
```html
<link data-trunk rel="rust" data-keep-debug="true" data-wasm-opt="z"/>
```
This instructs the rust `trunk` utility to pass a long an option to `wasm-bindgen` called `--keep-debug`
This option bundles in a type of sourcemap into the built wasm file.
Be aware that this will make the wasm file much larger.
### Browser Changes
Next we need to allow the browser to read the DWARF data from the wasm file.
For Chrome / Opera there's an extension here that needs to be installed.
* https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb?pli=1
## Debugging within the Browser
Within the browser's dev console it should now be possible to view the rust source code and add breakpoints.
![Chrome Debug Image](./img/breakpoint1.png)
## Debugging within VSCode
Note this is still experimental, although I have managed to get breakpoints working under VSCode.
So far I've only tried this within a windows environment.
In order to have the breakpoints land at the correct position.
We need to install the following VSCode extension.
* [WebAssembly DWARF Debugging](https://marketplace.visualstudio.com/items?itemName=ms-vscode.wasm-dwarf-debugging)
Within the browser launch section under `launch.json` we need to set userDataDir to false in order for the DWARF browser extension to be loaded.
```json
{
"name": "Launch Browser Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}/dist",
// Needed to keep the dwarf extension in the browser
"userDataDir": false,
},
```
Now we should be able to add breakpoints within visual studio code while debugging the rust wasm.
![Chrome Debug Image](./img/breakpoint2.png)

View File

@@ -0,0 +1,4 @@
[serve]
address = "127.0.0.1"
port = 4001
open = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-keep-debug="true" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "stable" # test change

View File

@@ -0,0 +1,29 @@
use leptos::*;
/// A simple counter component.
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32,
) -> impl IntoView {
let (value, set_value) = create_signal(initial_value);
view! {
<div>
<button on:click=move |_| set_value.set(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| {
// Test Panic
//panic!("Test Panic");
// In order to breakpoint the below, the code needs to be on it's own line
set_value.update(|value| *value += step)
}
>"+1"</button>
</div>
}
}

View File

@@ -0,0 +1,15 @@
use counter_dwarf_debug::SimpleCounter;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! {
<SimpleCounter
initial_value=0
step=1
/>
}
})
}

View File

@@ -0,0 +1,15 @@
# Generated by Cargo
# will have compiled files and executables
/target/
pkg
# These are backup files generated by rustfmt
**/*.rs.bk
# node e2e test tools and outputs
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
.secret_key

View File

@@ -0,0 +1,117 @@
[package]
name = "openapi-openai-api-swagger-ui"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos = { version = "0.6", features = ["nightly"] }
leptos_axum = { version = "0.6", optional = true }
leptos_meta = { version = "0.6", features = ["nightly"] }
leptos_router = { version = "0.6", features = ["nightly"] }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.92"
thiserror = "1"
tracing = { version = "0.1", optional = true }
utoipa = { version = "4.2.0", optional = true, features=["debug"] }
utoipa-swagger-ui = { version = "6.0.0", optional = true , features = ["axum"]}
http = "1"
serde = "1.0.198"
serde_json = {version = "1.0.116", optional = true}
openai_dive = {version="0.4.7",optional=true}
reqwest = "0.12.4"
uuid = { version = "1.8.0", features = ["v4"]}
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:openai_dive",
"dep:serde_json",
"dep:utoipa-swagger-ui",
"dep:utoipa",
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tracing",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "openapi-swagger-ui"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -0,0 +1,15 @@
#OpenAPI Swagger-Ui OpenAI GPT
This example shows how to document server functions via OpenAPI schema generated using Utoipa and serve the swagger ui via /swagger-ui endpoint. More than that, this example shows how to take said OpenAPI spec and turn it into a function list to feed to OpenAI's chat completion endpoint to generate the JSON values to feed back into our server functions.
The example shows an input and if you tell it to do something that is covered, say hello, or generate a list of names it will do that.
To use the AI part of this project provide your openAPI key in an environment variable when running cargo leptos.
```sh
OPENAI_API_KEY=my_secret_key cargo leptos serve
```
## Thoughts, Feedback, Criticism, Comments?
Send me any of the above, I'm @sjud on leptos discord. I'm always looking to improve and make these projects more helpful for the community. So please let me know how I can do that. Thanks!

View File

@@ -0,0 +1,74 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@@ -0,0 +1,107 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,174 @@
use crate::error_template::{AppError, ErrorTemplate};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/openapi-swagger-ui.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router fallback=|| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! {
<ErrorTemplate outside_errors/>
}
.into_view()
}>
<main>
<Routes>
<Route path="" view=HomePage/>
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let hello = Action::<HelloWorld,_>::server();
view! {
<button on:click = move |_| hello.dispatch(HelloWorld{say_whut:SayHello{say:true}})>
"hello world"
</button>
<ErrorBoundary
fallback=|err| view! { <p>{format!("{err:#?}")}</p>}>
{
move || hello.value().get().map(|h|match h {
Ok(h) => h.into_view(),
err => err.into_view()
})
}
</ErrorBoundary>
<AiSayHello/>
}
}
#[cfg_attr(feature="ssr",derive(utoipa::ToSchema))]
#[derive(Debug,Copy,Clone,serde::Serialize,serde::Deserialize)]
pub struct SayHello {
say:bool,
}
// the following function comment is what our GPT will get
/// Call to say hello world, or call to not say hello world.
#[cfg_attr(feature="ssr",utoipa::path(
post,
path = "/api/hello_world",
responses(
(status = 200, description = "Hello world from server or maybe not?", body = String),
),
params(
("say_whut" = SayHello, description = "If true then say hello, if false then don't."),
)
))]
#[server(
// we need to encoude our server functions as json because that's what openai generates
input=server_fn::codec::Json,
endpoint="hello_world"
)]
pub async fn hello_world(say_whut:SayHello) -> Result<String,ServerFnError> {
if say_whut.say {
Ok("hello world".to_string())
} else {
Ok("not hello".to_string())
}
}
/// Takes a list of names
#[cfg_attr(feature="ssr",utoipa::path(
post,
path = "/api/name_list",
responses(
(status = 200, description = "The same list you got back", body = String),
),
params(
("list" = Vec<String>, description = "A list of names"),
)
))]
#[server(
input=server_fn::codec::Json,
endpoint="name_list"
)]
pub async fn name_list(list:Vec<String>) -> Result<Vec<String>,ServerFnError> {
Ok(list)
}
#[derive(Clone,Debug,PartialEq,serde::Serialize,serde::Deserialize)]
pub struct AiServerCall{
pub path:String,
pub args:String,
}
// Don't include our AI function in the OpenAPI
#[server]
pub async fn ai_msg(msg:String) -> Result<AiServerCall,ServerFnError> {
crate::open_ai::call_gpt_with_api(msg).await.get(0).cloned().ok_or(ServerFnError::new("No first message"))
}
#[component]
pub fn AiSayHello() -> impl IntoView {
let ai_msg = Action::<AiMsg, _>::server();
let result = create_rw_signal(Vec::new());
view!{
<ActionForm action=ai_msg>
<label> "Tell the AI what function to call."
<input name="msg"/>
</label>
<input type="submit"/>
</ActionForm>
<div>
{
move || if let Some(Ok(AiServerCall{path,args})) = ai_msg.value().get() {
spawn_local(async move {
let text =
reqwest::Client::new()
.post(format!("http://127.0.0.1:3000/api/{}",path))
.header("content-type","application/json")
.body(args)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
result.update(|list|
list.push(
text
)
);
});
}
}
<For
each=move || result.get()
key=|_| uuid::Uuid::new_v4()
children=move |s:String| {
view! {
<p>{s}</p>
}
}
/>
</div>
}
}

View File

@@ -0,0 +1,72 @@
use http::status::StatusCode;
use leptos::*;
use thiserror::Error;
#[derive(Clone, Debug, Error)]
pub enum AppError {
#[error("Not Found")]
NotFound,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
}
}
}
// A basic function to display errors served by the error boundaries.
// Feel free to do more complicated things here than just displaying the error.
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get_untracked();
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
#[cfg(feature = "ssr")]
{
use leptos_axum::ResponseOptions;
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
view! {
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

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

View File

@@ -0,0 +1,27 @@
pub mod app;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fileserv;
#[cfg(feature="ssr")]
pub mod open_ai;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}
#[cfg(feature="ssr")]
pub mod api_doc {
use crate::app::__path_hello_world;
use crate::app::SayHello;
use crate::app::__path_name_list;
#[derive(utoipa::OpenApi)]
#[openapi(
info(description = "My Api description"),
paths(hello_world,name_list), components(schemas(SayHello)),
)]
pub struct ApiDoc;
}

View File

@@ -0,0 +1,42 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use openapi_swagger_ui::app::*;
use openapi_swagger_ui::api_doc::ApiDoc;
use openapi_swagger_ui::fileserv::file_and_error_handler;
use utoipa::OpenApi;
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.merge(utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()))
.with_state(leptos_options);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for a purely client-side app
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,267 @@
/*
Follows
https://cookbook.openai.com/examples/function_calling_with_an_openapi_spec
closely
*/
pub static SYSTEM_MESSAGE :&'static str = "
You are a helpful assistant.
Respond to the following prompt by using function_call and then summarize actions.
Ask for clarification if a user request is ambiguous.
";
use serde_json::Map;
use openai_dive::v1::api::Client;
use openai_dive::v1::models::Gpt4Engine;
use std::env;
use openai_dive::v1::resources::chat::{
ChatCompletionFunction, ChatCompletionParameters, ChatCompletionTool, ChatCompletionToolType, ChatMessage,
ChatMessageContent,Role,
};
use utoipa::openapi::schema::Array;
use serde_json::Value;
use utoipa::openapi::schema::SchemaType;
use utoipa::openapi::schema::Schema;
use utoipa::OpenApi;
use serde_json::json;
use utoipa::openapi::path::{PathItemType,Parameter};
use utoipa::openapi::Required;
use utoipa::openapi::schema::Object;
use utoipa::openapi::RefOr;
pub fn make_openapi_call_via_gpt(message:String) -> ChatCompletionParameters {
let docs = super::api_doc::ApiDoc::openapi();
let mut functions = vec![];
// get each path and it's path item object
for (path,path_item) in docs.paths.paths.iter(){
// all our server functions are post.
let operation = path_item.operations.get(&PathItemType::Post).expect("Expect POST op");
// This name will be given to the OpenAI API as part of our functions
let name = operation.operation_id.clone().expect("Each operation to have an operation id");
// we'll use the descrition
let desc = operation.description.clone().expect("Each operation to have a description, this is how GPT knows what the functiond does and it is helpful for calling it.");
let mut required_list = vec![];
let mut properties = serde_json::Map::new();
if let Some(params) = operation.parameters.clone() {
leptos::logging::log!("{params:#?}");
for Parameter{name,description,required,schema,..} in params.into_iter() {
if required == Required::True {
required_list.push(name.clone());
}
let description = description.unwrap_or_default();
if let Some(RefOr::Ref(utoipa::openapi::schema::Ref{ref_location,..})) = schema {
let schema_name = ref_location.split('/').last().expect("Expecting last after split");
let RefOr::T(schema) = docs.components
.as_ref()
.expect("components")
.schemas
.get(schema_name)
.cloned()
.expect("{schema_name} to be in components as a schema") else {panic!("expecting T")};
let mut output = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut output);
properties.insert(name,serde_json::Value::Object(output));
} else if let Some(RefOr::T(schema)) = schema {
let mut output = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut output);
properties.insert(name.clone(),serde_json::Value::Object(output));
}
}
}
let parameters = json!({
"type": "object",
"properties": properties,
"required": required_list,
});
leptos::logging::log!("{parameters}");
functions.push(
ChatCompletionFunction {
name,
description: Some(desc),
parameters,
}
)
}
ChatCompletionParameters {
model: Gpt4Engine::Gpt41106Preview.to_string(),
messages: vec![
ChatMessage {
role:Role::System,
content: ChatMessageContent::Text(SYSTEM_MESSAGE.to_string()),
..Default::default()
},
ChatMessage {
role:Role::User,
content: ChatMessageContent::Text(message),
..Default::default()
}],
tools: Some(functions.into_iter().map(|function|{
ChatCompletionTool {
r#type: ChatCompletionToolType::Function,
function,
}
}).collect::<Vec<ChatCompletionTool>>()),
..Default::default()
}
}
pub fn parse_schema_into_openapi_property(
name:String,
schema:Schema,
output: &mut serde_json::Map::<String,serde_json::Value>) {
let docs = super::api_doc::ApiDoc::openapi();
match schema {
Schema::Object(Object{
schema_type,
required,
properties,
..
}) => match schema_type{
SchemaType::Object => {
output.insert("type".to_string(),Value::String("object".to_string()));
output.insert("required".to_string(),Value::Array(required.into_iter()
.map(|s|Value::String(s))
.collect::<Vec<Value>>()));
output.insert("properties".to_string(),{
let mut map = Map::new();
for (key,val) in properties
.into_iter()
.map(|(key,val)|{
let RefOr::T(schema) = val else {panic!("expecting t")};
let mut output = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut output);
(key,output)
}) {
map.insert(key,Value::Object(val));
}
Value::Object(map)
});
},
SchemaType::Value => {
panic!("not expecting Value here.");
},
SchemaType::String => {
output.insert("type".to_string(),serde_json::Value::String("string".to_string()));
},
SchemaType::Integer => {
output.insert("type".to_string(),serde_json::Value::String("integer".to_string()));
},
SchemaType::Number => {
output.insert("type".to_string(),serde_json::Value::String("number".to_string()));
},
SchemaType::Boolean => {
output.insert("type".to_string(),serde_json::Value::String("boolean".to_string()));
},
SchemaType::Array => {
output.insert("type".to_string(),serde_json::Value::String("array".to_string()));
},
},
Schema::Array(Array{schema_type,items,..}) => {
match schema_type {
SchemaType::Array => {
let mut map = Map::new();
if let RefOr::Ref(utoipa::openapi::schema::Ref{ref_location,..}) = *items {
let schema_name = ref_location.split('/').last().expect("Expecting last after split");
let RefOr::T(schema) = docs.components
.as_ref()
.expect("components")
.schemas
.get(schema_name)
.cloned()
.expect("{schema_name} to be in components as a schema") else {panic!("expecting T")};
let mut map = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut map);
output.insert(name.clone(),serde_json::Value::Object(map));
} else if let RefOr::T(schema) = *items {
let mut map = Map::new();
parse_schema_into_openapi_property(name.clone(),schema,&mut map);
output.insert(name,serde_json::Value::Object(map));
}
},
_ => panic!("if schema is an array, then I'm expecting schema type to be an array ")
}
}
_ => panic!("I don't know how to handle this yet.")
}
}
// let docs = super::api_doc::ApiDoc::openapi();
use crate::app::AiServerCall;
pub async fn call_gpt_with_api(message:String) -> Vec<AiServerCall> {
let api_key = std::env::var("OPENAI_API_KEY").expect("$OPENAI_API_KEY is not set");
let client = Client::new(api_key);
let completion_parameters = make_openapi_call_via_gpt(message);
let result = client.chat().create(completion_parameters).await.unwrap();
let message = result.choices[0].message.clone();
let mut res = vec![];
if let Some(tool_calls) = message.clone().tool_calls {
for tool_call in tool_calls {
let name = tool_call.function.name;
let arguments = tool_call.function.arguments;
res.push(AiServerCall{
path:name,
args:arguments,
});
}
}
res
}
/*
def openapi_to_functions(openapi_spec):
functions = []
for path, methods in openapi_spec["paths"].items():
for method, spec_with_ref in methods.items():
# 1. Resolve JSON references.
spec = jsonref.replace_refs(spec_with_ref)
# 2. Extract a name for the functions.
function_name = spec.get("operationId")
# 3. Extract a description and parameters.
desc = spec.get("description") or spec.get("summary", "")
schema = {"type": "object", "properties": {}}
req_body = (
spec.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema")
)
if req_body:
schema["properties"]["requestBody"] = req_body
params = spec.get("parameters", [])
if params:
param_properties = {
param["name"]: param["schema"]
for param in params
if "schema" in param
}
schema["properties"]["parameters"] = {
"type": "object",
"properties": param_properties,
}
functions.append(
{"type": "function", "function": {"name": function_name, "description": desc, "parameters": schema}}
)
return functions */

View File

@@ -0,0 +1,4 @@
body {
font-family: sans-serif;
text-align: center;
}

View File

@@ -0,0 +1,4 @@
POSTGRES_DB=blogs
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=disable

2
projects/sitemap_axum/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
Cargo.lock

View File

@@ -0,0 +1,124 @@
[workspace]
# The empty workspace here is to keep rust-analyzer satisfied
[package]
name = "sitemap-axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos = { version = "0.6", features = ["nightly"] }
leptos_axum = { version = "0.6", optional = true }
leptos_meta = { version = "0.6", features = ["nightly"] }
leptos_router = { version = "0.6", features = ["nightly"] }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.92"
thiserror = "1"
tracing = { version = "0.1", optional = true }
http = "1"
# Example specific crates
sqlx = { version = "0.7", features = [
"postgres",
"runtime-tokio",
"tls-rustls",
"time",
], optional = true }
xml = { version = "0.8", optional = true }
time = { version = "0.3", features = ["macros", "serde", "formatting"] }
dotenvy = "0.15"
anyhow = "1"
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tracing",
"dep:sqlx",
"dep:xml",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "sitemap-axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
# style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
# end2end-cmd = "npx playwright test"
# end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -0,0 +1,36 @@
[tasks.run]
command = "cargo"
args = ["leptos", "watch"]
dependencies = ["start-db"]
[tasks.start-db]
command = "docker"
args = ["start", "blog_db"]
[tasks.run-db]
command = "docker"
args = [
"run",
"-d",
"--name",
"blog_db",
"-p",
"5432:5432",
"--env-file",
"./.env",
"-v",
"./init:/docker-entrypoint-initdb.d",
"postgres:latest",
]
[tasks.stop-db]
command = "docker"
args = ["stop", "blog_db"]
[tasks.drop-db]
command = "docker"
args = ["rm", "blog_db"]
dependencies = ["stop-db"]
[tasks.restart-db]
dependencies = ["drop-db", "start-db"]

View File

@@ -0,0 +1,20 @@
# Sitemaps with Axum
This project demonstrates how to serve a [sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview) file using Axum using dynamic data (like blog posts in this case). An example Postgres database is used data source for storing blog post data that can be used to generate a dynamic site map based on blog post slugs. There's lots of [sitemap crates](https://crates.io/search?q=sitemap), though this example uses the [xml](https://crates.io/crates/xml) for example purposes.
## Quick Start
We use Docker to provide a Postgres database for this sample, so make sure you have it installed.
```sh
$ docker -v
Docker version 25.0.3, build 4debf41
```
Once Docker has started on you local machine, run (make sure to have `cargo-make` installed):
```sh
$ cargo make run
```
This will handle spinning up a Postgres container, initializing the example database, and launching the local dev server.

View File

@@ -0,0 +1,32 @@
-- The database initialization script is used for defining your local schema as well as postgres
-- running within a docker container, where we'll copy this file over and run on startup
DO
$$
BEGIN
IF
NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'blogs') THEN
CREATE DATABASE blogs;
END IF;
END
$$;
\c blogs;
DROP TABLE IF EXISTS posts;
CREATE TABLE posts
(
id SERIAL PRIMARY KEY,
slug VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
content VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO posts (slug, title, content)
VALUES ('first-post', 'First Post', 'This is the content of the first post.'),
('second-post', 'Second Post', 'Here is some more content for another post.'),
('hello-world', 'Hello World', 'Yet another post to add to our collection.'),
('tech-talk', 'Tech Talk', 'Discussing the latest in technology.'),
('travel-diaries', 'Travel Diaries', 'Sharing my experiences traveling around the world.');

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
<url>
<loc>https://mywebsite.com/blog/first-post</loc>
<lastmod>2024-04-23T17:28:07Z</lastmod>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mywebsite.com/blog/second-post</loc>
<lastmod>2024-04-23T17:28:07Z</lastmod>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mywebsite.com/blog/hello-world</loc>
<lastmod>2024-04-23T17:28:07Z</lastmod>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mywebsite.com/blog/tech-talk</loc>
<lastmod>2024-04-23T17:28:07Z</lastmod>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mywebsite.com/blog/travel-diaries</loc>
<lastmod>2024-04-23T17:28:07Z</lastmod>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://mywebsite.com</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
<url>
<loc>https://mywebsite.com</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://mywebsite.com/about</loc>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
</url>
</urlset>

View File

@@ -0,0 +1,48 @@
use crate::error_template::{AppError, ErrorTemplate};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/sitemap-axum.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router fallback=|| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! {
<ErrorTemplate outside_errors/>
}
.into_view()
}>
<main>
<Routes>
<Route path="" view=HomePage/>
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
view! {
<h1>"Welcome to Leptos!"</h1>
// Typically, you won't route to these files manually - a crawler of sorts will take care of that
<a href="http://localhost:3000/sitemap-index.xml">"Generate dynamic sitemap"</a>
<a style="padding-left: 1em;" href="http://localhost:3000/sitemap-static.xml">"Go to static sitemap"</a>
}
}

View File

@@ -0,0 +1,72 @@
use http::status::StatusCode;
use leptos::*;
use thiserror::Error;
#[derive(Clone, Debug, Error)]
pub enum AppError {
#[error("Not Found")]
NotFound,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
}
}
}
// A basic function to display errors served by the error boundaries.
// Feel free to do more complicated things here than just displaying the error.
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get_untracked();
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
#[cfg(feature = "ssr")]
{
use leptos_axum::ResponseOptions;
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
view! {
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

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

View File

@@ -0,0 +1,14 @@
pub mod app;
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod fileserv;
#[cfg(feature = "ssr")]
pub mod sitemap;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View File

@@ -0,0 +1,47 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::get, Router};
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use sitemap_axum::{
app::*, fileserv::file_and_error_handler, sitemap::generate_sitemap,
};
use tower_http::services::ServeFile;
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
// Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
// Build our application with a route
let app = Router::new()
// We can use Axum to mount a route that serves a sitemap file that we can generate with dynamic data
.route("/sitemap-index.xml", get(generate_sitemap))
// Using tower's serve file service, we can also serve a static sitemap file for relatively small sites too
.route_service(
"/sitemap-static.xml",
ServeFile::new("sitemap-static.xml"),
)
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for a purely client-side app
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,174 @@
use axum::{
body::Body,
response::{IntoResponse, Response},
};
use sqlx::{PgPool, Pool, Postgres};
use std::{
env::{self, current_dir},
fs::File,
io::{BufWriter, Read},
path::Path,
};
use time::{format_description, PrimitiveDateTime};
use xml::{writer::XmlEvent, EmitterConfig, EventWriter};
#[derive(Debug)]
struct Post {
slug: String,
updated_at: PrimitiveDateTime,
}
/// Generates a sitemap based on data stored in a database containing slugs that we can use to build URLs to the posts themselves.
pub async fn generate_sitemap() -> impl IntoResponse {
dotenvy::dotenv().ok();
// Depending on your preference, we can dynamically servce the sitemap file for each,
// or generate the file once on the first visit (probably from a bot) and write it to disk
// so we can simply serve the created file instead of having to query the database every time
let sitemap_path = format!(
"{}/sitemap-index.xml",
&current_dir().unwrap().to_str().unwrap()
);
let path = Path::new(&sitemap_path);
// If the doesn't exist, we've probably deployed a fresh version of our Leptos site somewhere so we'll generate it on first request
if !path.exists() {
let pool = PgPool::connect(
&env::var("DATABASE_URL").expect("database URL to exist"),
)
.await
.expect("to be able to connect to pool");
create_sitemap_file(path, pool).await.ok();
}
// Once the file has been written, grab the contents of it and write it out as an XML file in the response
let mut file = File::open(sitemap_path).unwrap();
let mut contents = vec![];
file.read_to_end(&mut contents).ok();
let body = Body::from(contents);
Response::builder()
.header("Content-Type", "application/xml")
// Cache control can be helpful for cases where your site might be deployed occassionally and the original
// sitemap that was generated can be cached with a header
.header("Cache-Control", "max-age=86400")
.body(body)
.unwrap()
}
async fn create_sitemap_file(
path: &Path,
pool: Pool<Postgres>,
) -> anyhow::Result<()> {
let file = File::create(path).expect("sitemap file to be created");
let file = BufWriter::new(file);
let mut writer = EmitterConfig::new()
.perform_indent(true)
.create_writer(file);
writer
.write(
XmlEvent::start_element("urlset")
.attr("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
.attr("xmlns:xhtml", "http://www.w3.org/1999/xhtml")
.attr(
"xmlns:image",
"http://www.google.com/schemas/sitemap-image/1.1",
)
.attr(
"xmlns:video",
"http://www.google.com/schemas/sitemap-video/1.1",
)
.attr(
"xmlns:news",
"http://www.google.com/schemas/sitemap-news/0.9",
),
)
.expect("xml header to be written");
// We could also pull this from configuration or an environment variable
let app_url = "https://mywebsite.com";
// First, read all the blog entries so we can get the slug for building,
// URLs and the updated date to determine the change frequency
sqlx::query_as!(
Post,
r#"
SELECT slug,
updated_at
FROM posts
ORDER BY updated_at DESC
"#
)
.fetch_all(&pool)
.await
.expect("")
.into_iter()
.try_for_each(|p| write_post_entry(p, app_url, &mut writer))?;
// Next, write the static pages and close the XML stream
write_static_page_entry(app_url, &mut writer)?;
writer.write(XmlEvent::end_element())?;
Ok(())
}
fn write_post_entry(
post: Post,
app_url: &str,
writer: &mut EventWriter<BufWriter<File>>,
) -> anyhow::Result<()> {
let format = format_description::parse(
"[year]-[month]-[day]T[hour]:[minute]:[second]Z",
)?;
let parsed_date = post.updated_at.format(&format)?;
let route = format!("{}/blog/{}", app_url, post.slug);
writer.write(XmlEvent::start_element("url"))?;
writer.write(XmlEvent::start_element("loc"))?;
writer.write(XmlEvent::characters(&route))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("lastmod"))?;
writer.write(XmlEvent::characters(&parsed_date))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("changefreq"))?;
writer.write(XmlEvent::characters("yearly"))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("priority"))?;
writer.write(XmlEvent::characters("0.5"))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::end_element())?;
Ok(())
}
fn write_static_page_entry(
route: &str,
writer: &mut EventWriter<BufWriter<File>>,
) -> anyhow::Result<()> {
write_entry(route, "weekly", "0.8", writer)?;
Ok(())
}
fn write_entry(
route: &str,
change_frequency: &str,
priority: &str,
writer: &mut EventWriter<BufWriter<File>>,
) -> anyhow::Result<()> {
writer.write(XmlEvent::start_element("url"))?;
writer.write(XmlEvent::start_element("loc"))?;
writer.write(XmlEvent::characters(route))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("changefreq"))?;
writer.write(XmlEvent::characters(change_frequency))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("priority"))?;
writer.write(XmlEvent::characters(priority))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::end_element())?;
Ok(())
}

View File

@@ -30,8 +30,6 @@ impl ParamsMap {
/// Inserts a value into the map.
#[inline(always)]
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
use crate::history::url::unescape;
let value = unescape(&value);
self.0.insert(key, value)
}

View File

@@ -25,7 +25,7 @@ pub fn unescape(s: &str) -> String {
#[cfg(not(feature = "ssr"))]
pub fn unescape(s: &str) -> String {
js_sys::decode_uri(s).unwrap().into()
js_sys::decode_uri_component(s).unwrap().into()
}
#[cfg(feature = "ssr")]
@@ -36,7 +36,7 @@ pub fn escape(s: &str) -> String {
#[cfg(not(feature = "ssr"))]
pub fn escape(s: &str) -> String {
js_sys::encode_uri(s).as_string().unwrap()
js_sys::encode_uri_component(s).as_string().unwrap()
}
#[cfg(not(feature = "ssr"))]

View File

@@ -1,7 +1,7 @@
// Implementation based on Solid Router
// see <https://github.com/solidjs/solid-router/blob/main/src/utils.ts>
use crate::ParamsMap;
use crate::{unescape, ParamsMap};
#[derive(Debug, Clone, PartialEq, Eq)]
#[doc(hidden)]
@@ -68,7 +68,10 @@ impl Matcher {
self.segments.iter().zip(loc_segments.iter())
{
if let Some(param_name) = segment.strip_prefix(':') {
params.insert(param_name.into(), (*loc_segment).into());
params.insert(
param_name.into(),
unescape(*loc_segment).into(),
);
} else if segment != loc_segment {
// if any segment doesn't match and isn't a param, there's no path match
return None;

View File

@@ -33,6 +33,7 @@ use syn::__private::ToTokens;
/// - `endpoint`: specifies the exact path at which the server function handler will be mounted,
/// relative to the prefix (defaults to the function name followed by unique hash)
/// - `input`: the encoding for the arguments (defaults to `PostUrl`)
/// - `input_derive`: a list of derives to be added on the generated input struct (defaults to `(Clone, serde::Serialize, serde::Deserialize)` if `input` is set to a custom struct, won't have an effect otherwise)
/// - `output`: the encoding for the response (defaults to `Json`)
/// - `client`: a custom `Client` implementation that will be used for this server fn
/// - `encoding`: (legacy, may be deprecated in future) specifies the encoding, which may be one

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