Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
6ac55266f8 v0.4.0 2023-06-29 15:45:21 -04:00
31 changed files with 82 additions and 365 deletions

View File

@@ -31,7 +31,7 @@ cargo init leptos-tutorial
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
cargo add leptos
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
import { CountersPage } from "./counters_page";
test.describe("Add 1000 Counters", () => {
test("should increase the number of counters", async ({ page }) => {
test("should increment the total count by 1K", async ({ page }) => {
const ui = new CountersPage(page);
await Promise.all([

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
import { CountersPage } from "./counters_page";
test.describe("Add Counter", () => {
test("should increase the number of counters", async ({ page }) => {
test("should increment the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();

View File

@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
import { CountersPage } from "./counters_page";
test.describe("Clear Counters", () => {
test("should reset the counts", async ({ page }) => {

View File

@@ -5,11 +5,8 @@ export class CountersPage {
readonly addCounterButton: Locator;
readonly addOneThousandCountersButton: Locator;
readonly clearCountersButton: Locator;
readonly incrementCountButton: Locator;
readonly counterInput: Locator;
readonly decrementCountButton: Locator;
readonly removeCountButton: Locator;
readonly incrementCountButton: Locator;
readonly total: Locator;
readonly counters: Locator;
@@ -35,15 +32,9 @@ export class CountersPage {
hasText: "+1",
});
this.removeCountButton = page.locator("button", {
hasText: "x",
});
this.total = page.getByTestId("total");
this.counters = page.getByTestId("counters");
this.counterInput = page.getByRole("textbox");
}
async goto() {
@@ -61,17 +52,17 @@ export class CountersPage {
this.addOneThousandCountersButton.click();
}
async decrementCount(index: number = 0) {
async decrementCount() {
await Promise.all([
this.decrementCountButton.nth(index).waitFor(),
this.decrementCountButton.nth(index).click(),
this.decrementCountButton.waitFor(),
this.decrementCountButton.click(),
]);
}
async incrementCount(index: number = 0) {
async incrementCount() {
await Promise.all([
this.incrementCountButton.nth(index).waitFor(),
this.incrementCountButton.nth(index).click(),
this.incrementCountButton.waitFor(),
this.incrementCountButton.click(),
]);
}
@@ -81,18 +72,4 @@ export class CountersPage {
this.clearCountersButton.click(),
]);
}
async enterCount(count: string, index: number = 0) {
await Promise.all([
this.counterInput.nth(index).waitFor(),
this.counterInput.nth(index).fill(count),
]);
}
async removeCounter(index: number = 0) {
await Promise.all([
this.removeCountButton.nth(index).waitFor(),
this.removeCountButton.nth(index).click(),
]);
}
}

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
import { CountersPage } from "./counters_page";
test.describe("Decrement Count", () => {
test("should decrease the total count", async ({ page }) => {
test("should decrement the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();

View File

@@ -1,31 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Enter Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.enterCount("5");
await expect(ui.total).toHaveText("5");
await expect(ui.counters).toHaveText("1");
});
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.enterCount("100");
await ui.enterCount("100", 1);
await ui.enterCount("100", 2);
await ui.enterCount("50", 1);
await expect(ui.total).toHaveText("250");
await expect(ui.counters).toHaveText("3");
});
});

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
import { CountersPage } from "./counters_page";
test.describe("Increment Count", () => {
test("should increase the total count", async ({ page }) => {
test("should increment the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();

View File

@@ -1,18 +0,0 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Remove Counter", () => {
test("should decrement the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.removeCounter(1);
await expect(ui.total).toHaveText("0");
await expect(ui.counters).toHaveText("2");
});
});

View File

@@ -1,8 +1,8 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
import { CountersPage } from "./counters_page";
test.describe("View Counters", () => {
test("should see the title", async ({ page }) => {
test("should_see_the_title", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();

View File

@@ -56,7 +56,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
</button>
<p>
"Total: "
<span data-testid="total">{move ||
<span id="total" data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
@@ -64,7 +64,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
<span id="counters" data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
@@ -104,7 +104,7 @@ fn Counter(
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<span>{move || value.get().to_string()}</span>
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>

View File

@@ -1,21 +0,0 @@
Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml
# Support trunk
dist

View File

@@ -1,8 +1,4 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]
extend = { path = "../cargo-make/main.toml" }
[tasks.build]
command = "cargo"

View File

@@ -1,36 +0,0 @@
{
"name": "e2e",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@playwright/test": {
"version": "1.35.1",
"dev": true,
"requires": {
"@types/node": "*",
"fsevents": "2.3.2",
"playwright-core": "1.35.1"
}
},
"@types/node": {
"version": "20.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
"integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright-core": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
"dev": true
}
}
}

View File

@@ -1,7 +0,0 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.35.1"
}
}

View File

@@ -1,77 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* 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 ? 10 : 10,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "list",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* 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: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "cd ../ && trunk serve",
// url: "http://127.0.0.1:8080",
// reuseExistingServer: false, //!process.env.CI,
// },
});

View File

@@ -1,30 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("Test Router example", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
const links = [
{ label: "Bill Smith", url: "/0" },
{ label: "Tim Jones", url: "/1" },
{ label: "Sally Stevens", url: "/2" },
{ label: "About", url: "/about" },
{ label: "Settings", url: "/settings" },
];
links.forEach(({ label, url }) => {
test(`Can navigate to ${label}`, async ({ page }) => {
await page.getByRole("link", { name: label }).click();
await expect(page.getByRole("heading", { name: label })).toBeVisible();
await expect(page).toHaveURL(url);
});
});
test("Can redirect to home", async ({ page }) => {
await page.getByRole("link", { name: "About" }).click();
await page.getByRole("link", { name: "Redirect to Home" }).click();
await expect(page).toHaveURL("/");
});
});

View File

@@ -1,11 +0,0 @@
{
"private": true,
"scripts": {
"start-server": "trunk serve",
"e2e": "cargo make test-playwright",
"e2e:auto-start": "start-server-and-test start-server http://127.0.0.1:8080 e2e"
},
"devDependencies": {
"start-server-and-test": "^2.0.0"
}
}

View File

@@ -52,9 +52,8 @@ If you're using VS Code, add the following to your `settings.json`
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install [VS Browser](https://marketplace.visualstudio.com/items?itemName=Phu1237.vs-browser) extension (allows you to open a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling

View File

@@ -73,7 +73,7 @@ where
let child = DynChild::new({
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
let current_id = current_id;
let current_id = current_id.clone();
let children = Rc::new(orig_children(cx).into_view(cx));
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
@@ -94,10 +94,10 @@ where
// run the child; we'll probably throw this away, but it will register resource reads
//let after_original_child = HydrationCtx::peek();
{
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
HydrationCtx::continue_from(current_id);
HydrationCtx::continue_from(current_id.clone());
DynChild::new({
let children = Rc::clone(&children);
move || (*children).clone()
@@ -115,7 +115,9 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
HydrationCtx::continue_from(current_id);
HydrationCtx::continue_from(
current_id.clone(),
);
DynChild::new({
let orig_children =
orig_children(cx).into_view(cx);
@@ -130,7 +132,9 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
HydrationCtx::continue_from(current_id);
HydrationCtx::continue_from(
current_id.clone(),
);
DynChild::new({
let orig_children =
orig_children(cx).into_view(cx);
@@ -145,7 +149,9 @@ where
// return the fallback for now, wrapped in fragment identifier
fallback().into_view(cx)
}
}
};
initial
}
}
})

View File

@@ -153,30 +153,16 @@ impl TryFrom<String> for Env {
/// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file.
pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let re_workspace: Regex =
Regex::new(r#"(?m)^\[\[workspace.metadata.leptos\]\]"#).unwrap();
let metadata_name;
let start;
match re.find(text) {
Some(found) => {
metadata_name = "[package.metadata.leptos]";
start = found.start();
}
None => match re_workspace.find(text) {
Some(found) => {
metadata_name = "[[workspace.metadata.leptos]]";
start = found.start();
}
None => return Err(LeptosConfigError::ConfigSectionNotFound),
},
let start = match re.find(text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
};
// so that serde error messages have right line number
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input
.replace(metadata_name, "[leptos_options]")
.replace("[package.metadata.leptos]", "[leptos_options]")
.replace('-', "_");
let settings = Config::builder()
// Read the "default" configuration file

View File

@@ -33,7 +33,7 @@ pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
use leptos_reactive::Scope;
#[cfg(not(feature = "nightly"))]
#[cfg(feature = "stable")]
use leptos_reactive::{
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
@@ -143,7 +143,7 @@ where
}
}
#[cfg(not(feature = "nightly"))]
#[cfg(feature = "stable")]
impl<T> IntoView for ReadSignal<T>
where
T: IntoView + Clone,
@@ -156,7 +156,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(not(feature = "nightly"))]
#[cfg(feature = "stable")]
impl<T> IntoView for RwSignal<T>
where
T: IntoView + Clone,
@@ -169,7 +169,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(not(feature = "nightly"))]
#[cfg(feature = "stable")]
impl<T> IntoView for Memo<T>
where
T: IntoView + Clone,
@@ -182,7 +182,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(not(feature = "nightly"))]
#[cfg(feature = "stable")]
impl<T> IntoView for Signal<T>
where
T: IntoView + Clone,
@@ -195,7 +195,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(not(feature = "nightly"))]
#[cfg(feature = "stable")]
impl<T> IntoView for MaybeSignal<T>
where
T: IntoView + Clone,

View File

@@ -296,7 +296,7 @@ fn ooo_body_stream_recurse(
fragments.chain(resources).chain(
futures::stream::once(async move {
let pending = cx.pending_fragments();
if !pending.is_empty() {
if pending.len() > 0 {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {

View File

@@ -70,7 +70,7 @@ impl ViewMacros {
let mut views = Vec::new();
for view in visitor.views {
let span = view.span();
let id = span_to_stable_id(path, span.start().line);
let id = span_to_stable_id(path, span);
let mut tokens = view.tokens.clone().into_iter();
tokens.next(); // cx
tokens.next(); // ,
@@ -148,11 +148,15 @@ impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
}
}
pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
pub fn span_to_stable_id(
path: impl AsRef<Path>,
site: proc_macro2::Span,
) -> String {
let file = path
.as_ref()
.to_str()
.unwrap_or_default()
.replace(['/', '\\'], "-");
format!("{file}-{line}")
let start = site.start();
format!("{}-{:?}", file, start.line)
}

View File

@@ -3,7 +3,7 @@ function patch(json) {
try {
const views = JSON.parse(json);
for (const [id, patches] of views) {
console.log("[HOT RELOAD]", id, patches);
console.log("[HOT RELOAD]", patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;

View File

@@ -438,18 +438,15 @@ impl Docs {
let mut attrs = attrs
.iter()
.filter_map(|attr| {
let Meta::NameValue(attr) = &attr.meta else {
return None;
let Meta::NameValue(attr ) = &attr.meta else {
return None
};
if !attr.path.is_ident("doc") {
return None;
return None
}
let Some(val) = value_to_string(&attr.value) else {
abort!(
attr,
"expected string literal in value of doc comment"
);
abort!(attr, "expected string literal in value of doc comment");
};
Some((val, attr.path.span()))

View File

@@ -384,7 +384,7 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.start().line()
site.into()
))
} else {
_ = site;
@@ -793,18 +793,15 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, three, or four arguments to the server function:
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
///
/// ```rust,ignore
/// // will generate a server function at `/api-prefix/hello`
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
/// ```
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos
@@ -824,16 +821,17 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
///
/// Note the following:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// - **Return types must be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
@@ -842,9 +840,6 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
/// can be a Leptos `Scope`. This scope can be used to inject dependencies like the HTTP request
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
/// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this
/// is to use the `handle_server_fns` function from [`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.handle_server_fns.html)
/// or [`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.handle_server_fns.html).
///
/// ## Server Function Encodings
///

View File

@@ -582,13 +582,6 @@ fn attribute_to_tokens_ssr<'a>(
{
// ignore props for SSR
// ignore classes and styles: we'll handle these separately
if name.starts_with("prop:") {
let value = attr.value();
exprs_for_compiler.push(quote! {
#[allow(unused_braces)]
{ _ = #value; }
});
}
} else if name == "inner_html" {
return attr.value();
} else {
@@ -1374,8 +1367,8 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
let is_custom = event_type == "Custom";
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(event_type, "couldn't parse event name");
};
abort!(event_type, "couldn't parse event name");
};
let event_type = if is_custom {
quote! { Custom::new(#name) }
@@ -1404,10 +1397,7 @@ pub(crate) fn slot_to_tokens(
let span = node.name().span();
let Some(parent_slots) = parent_slots else {
proc_macro_error::emit_error!(
span,
"slots cannot be used inside HTML elements"
);
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
return;
};

View File

@@ -10,18 +10,16 @@ use syn::__private::ToTokens;
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled.
///
/// You can specify one, two, three, or four arguments to the server function:
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
///
/// ```rust,ignore
/// // will generate a server function at `/api-prefix/hello`
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
/// ```
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`.

View File

@@ -54,7 +54,7 @@ impl From<ServerFnError> for Error {
/// Unlike [`ServerFnErrorErr`], this does not implement [`std::error::Error`].
/// This means that other error types can easily be converted into it using the
/// `?` operator.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerFnError {
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
Registration(String),

View File

@@ -385,9 +385,9 @@ impl Parse for ServerFnBody {
let docs = attrs
.iter()
.filter_map(|attr| {
let Meta::NameValue(attr) = &attr.meta else {
return None;
};
let Meta::NameValue(attr ) = &attr.meta else {
return None
};
if !attr.path.is_ident("doc") {
return None;
}