Compare commits

...

14 Commits

Author SHA1 Message Date
Greg Johnston
61cd68314f cargo fmt 2023-07-02 17:23:00 -04:00
Martin
90470a6f2d Minor: Ran cargo clippy --fix. 2023-07-02 17:22:39 -04:00
Michael Zimmermann
8fb4e88439 feat: implement PartialEq on ServerFnError (#1260)
This allows returning it in a memo.
2023-07-02 17:22:06 -04:00
Greg Johnston
e821efca07 chore: new cargo fmt (#1266) 2023-07-02 17:01:39 -04:00
Sridhar Ratnakumar
568f7b21ae example/readme: Link to 'VS Browser' ext; format. (#1261) 2023-07-02 16:56:59 -04:00
Greg Johnston
d3c0f5320c docs: update 02_getting_started.md (#1256) 2023-06-30 17:26:33 -04:00
Greg Johnston
5adc88bf50 fix: hot-reloading view marker line number (#1255) 2023-06-30 14:03:54 -04:00
Greg Johnston
67300adf41 fix: regression in ability to use signals directly in the view in stable (#1254) 2023-06-30 11:59:29 -04:00
afiqzx
4a3a67bf37 feat: add fallback support for workspace in get_config_from_str (#1249) 2023-06-30 10:44:27 -04:00
Dương
8150847218 test(router_example): add playwright tests (#1247)
* implemented e2e tests for router example

* chore(router_example): cleanup e2e/package.json
2023-06-30 10:40:33 -04:00
Greg Johnston
8cb95b4646 docs: update server fn docs (#1252) 2023-06-30 10:40:06 -04:00
Joseph Cruz
df4ce904a0 test(counters_stable): add missing e2e tests (#1251)
* test(counters_stable): remove unused ids

* test(counters_stable): enter count

* refactor(counters_stable/e2e): improve test names

* refactor(counters_stable): move page object

* refactor(counters_stable/e2e): target nth counter

* test(counters_stable): remove counter

* refactor(counters_stable/e2e): change description
2023-06-30 08:52:48 -04:00
Ari Seyhun
1cc3a43268 chore: remove unused variable warnings with ssr props (#1244) 2023-06-30 08:05:20 -04:00
Greg Johnston
d5a862a406 v0.4.0 (#1250) 2023-06-30 07:51:07 -04:00
34 changed files with 382 additions and 99 deletions

View File

@@ -1,5 +1,5 @@
[workspace]
resolver="2"
resolver = "2"
members = [
# core
"leptos",
@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.3.0"
version = "0.4.0"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.3.0" }
leptos_dom = { path = "./leptos_dom", version = "0.3.0" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
leptos_macro = { path = "./leptos_macro", version = "0.3.0" }
leptos_reactive = { path = "./leptos_reactive", version = "0.3.0" }
leptos_server = { path = "./leptos_server", version = "0.3.0" }
server_fn = { path = "./server_fn", version = "0.3.0" }
server_fn_macro = { path = "./server_fn_macro", version = "0.3.0" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.3.0" }
leptos_config = { path = "./leptos_config", version = "0.3.0" }
leptos_router = { path = "./router", version = "0.3.0" }
leptos_meta = { path = "./meta", version = "0.3.0" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.4.0" }
leptos_dom = { path = "./leptos_dom", version = "0.4.0" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.0" }
leptos_macro = { path = "./leptos_macro", version = "0.4.0" }
leptos_reactive = { path = "./leptos_reactive", version = "0.4.0" }
leptos_server = { path = "./leptos_server", version = "0.4.0" }
server_fn = { path = "./server_fn", version = "0.4.0" }
server_fn_macro = { path = "./server_fn_macro", version = "0.4.0" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.0" }
leptos_config = { path = "./leptos_config", version = "0.4.0" }
leptos_router = { path = "./router", version = "0.4.0" }
leptos_meta = { path = "./meta", version = "0.4.0" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.0" }
[profile.release]
codegen-units = 1

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
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
```
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 "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add 1000 Counters", () => {
test("should increment the total count by 1K", async ({ page }) => {
test("should increase the number of counters", 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 "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add Counter", () => {
test("should increment the total count", async ({ page }) => {
test("should increase the number of counters", 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 "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Clear Counters", () => {
test("should reset the counts", async ({ page }) => {

View File

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

View File

@@ -0,0 +1,31 @@
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

@@ -5,8 +5,11 @@ export class CountersPage {
readonly addCounterButton: Locator;
readonly addOneThousandCountersButton: Locator;
readonly clearCountersButton: Locator;
readonly decrementCountButton: Locator;
readonly incrementCountButton: Locator;
readonly counterInput: Locator;
readonly decrementCountButton: Locator;
readonly removeCountButton: Locator;
readonly total: Locator;
readonly counters: Locator;
@@ -32,9 +35,15 @@ 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() {
@@ -52,17 +61,17 @@ export class CountersPage {
this.addOneThousandCountersButton.click();
}
async decrementCount() {
async decrementCount(index: number = 0) {
await Promise.all([
this.decrementCountButton.waitFor(),
this.decrementCountButton.click(),
this.decrementCountButton.nth(index).waitFor(),
this.decrementCountButton.nth(index).click(),
]);
}
async incrementCount() {
async incrementCount(index: number = 0) {
await Promise.all([
this.incrementCountButton.waitFor(),
this.incrementCountButton.click(),
this.incrementCountButton.nth(index).waitFor(),
this.incrementCountButton.nth(index).click(),
]);
}
@@ -72,4 +81,18 @@ 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 "./counters_page";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Increment Count", () => {
test("should increment the total count", async ({ page }) => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();

View File

@@ -0,0 +1,18 @@
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 "./counters_page";
import { CountersPage } from "./fixtures/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 id="total" data-testid="total">{move ||
<span 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 id="counters" data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
<span 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>{move || value.get().to_string()}</span>
<span>{value}</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>

21
examples/router/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
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,4 +1,8 @@
extend = { path = "../cargo-make/main.toml" }
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]
[tasks.build]
command = "cargo"

36
examples/router/e2e/package-lock.json generated Normal file
View File

@@ -0,0 +1,36 @@
{
"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

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

View File

@@ -0,0 +1,77 @@
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

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,11 @@
{
"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,8 +52,9 @@ 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" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
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.
## 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.clone();
let current_id = current_id;
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.clone());
HydrationCtx::continue_from(current_id);
DynChild::new({
let children = Rc::clone(&children);
move || (*children).clone()
@@ -115,9 +115,7 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
HydrationCtx::continue_from(
current_id.clone(),
);
HydrationCtx::continue_from(current_id);
DynChild::new({
let orig_children =
orig_children(cx).into_view(cx);
@@ -132,9 +130,7 @@ where
{
let orig_children = Rc::clone(&orig_children);
move || {
HydrationCtx::continue_from(
current_id.clone(),
);
HydrationCtx::continue_from(current_id);
DynChild::new({
let orig_children =
orig_children(cx).into_view(cx);
@@ -149,9 +145,7 @@ where
// return the fallback for now, wrapped in fragment identifier
fallback().into_view(cx)
}
};
initial
}
}
}
})

View File

@@ -153,16 +153,30 @@ 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 start = match re.find(text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
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),
},
};
// 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("[package.metadata.leptos]", "[leptos_options]")
.replace(metadata_name, "[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(feature = "stable")]
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
@@ -143,7 +143,7 @@ where
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for ReadSignal<T>
where
T: IntoView + Clone,
@@ -156,7 +156,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for RwSignal<T>
where
T: IntoView + Clone,
@@ -169,7 +169,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for Memo<T>
where
T: IntoView + Clone,
@@ -182,7 +182,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
impl<T> IntoView for Signal<T>
where
T: IntoView + Clone,
@@ -195,7 +195,7 @@ where
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
#[cfg(not(feature = "nightly"))]
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.len() > 0 {
if !pending.is_empty() {
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);
let id = span_to_stable_id(path, span.start().line);
let mut tokens = view.tokens.clone().into_iter();
tokens.next(); // cx
tokens.next(); // ,
@@ -148,15 +148,11 @@ impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
}
}
pub fn span_to_stable_id(
path: impl AsRef<Path>,
site: proc_macro2::Span,
) -> String {
pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
let file = path
.as_ref()
.to_str()
.unwrap_or_default()
.replace(['/', '\\'], "-");
let start = site.start();
format!("{}-{:?}", file, start.line)
format!("{file}-{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]", patches);
console.log("[HOT RELOAD]", id, patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;

View File

@@ -438,15 +438,18 @@ 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.into()
site.start().line()
))
} else {
_ = site;
@@ -793,15 +793,18 @@ 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, or three arguments to the server function:
/// You can specify one, two, three, or four 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*: 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"`.
/// 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")]
/// ```
///
/// 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
@@ -821,17 +824,16 @@ 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 be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.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 be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// - **Arguments must 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`
@@ -840,6 +842,9 @@ 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,6 +582,13 @@ 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 {
@@ -1367,8 +1374,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) }
@@ -1397,7 +1404,10 @@ 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

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

View File

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

View File

@@ -10,16 +10,18 @@ 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, or three arguments to the server function:
/// You can specify one, two, three, or four 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*: 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"`.
/// 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")]
/// ```
///
/// 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)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
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;
}