mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
19 Commits
2182
...
fix-effect
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49aa5179af | ||
|
|
20db2094a0 | ||
|
|
4f60651633 | ||
|
|
31fbf738c2 | ||
|
|
1a4236e100 | ||
|
|
051a8c8af9 | ||
|
|
58476bb98e | ||
|
|
88d4f14541 | ||
|
|
6bba233ba7 | ||
|
|
1d99764740 | ||
|
|
53cc479c14 | ||
|
|
d3707d9b88 | ||
|
|
1df4076fd2 | ||
|
|
28337bb6c9 | ||
|
|
c5dea52e69 | ||
|
|
d5096ff2e6 | ||
|
|
84734f1110 | ||
|
|
7094308287 | ||
|
|
3b88c8ccd2 |
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
|
||||
2
.github/workflows/get-example-changed.yml
vendored
2
.github/workflows/get-example-changed.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
|
||||
2
.github/workflows/get-leptos-changed.yml
vendored
2
.github/workflows/get-leptos-changed.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
any_error/**
|
||||
|
||||
@@ -28,3 +28,6 @@ futures-executor = ["futures/thread-pool", "futures/executor"]
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
6
examples/counters/.gitignore
vendored
Normal file
6
examples/counters/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
@@ -2,4 +2,5 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-trunk-test.toml" },
|
||||
]
|
||||
|
||||
4
examples/counters/e2e/.gitignore
vendored
Normal file
4
examples/counters/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/counters/e2e/package-lock.json
generated
Normal file
83
examples/counters/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
examples/counters/e2e/package.json
Normal file
10
examples/counters/e2e/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pnpm": "^9.7.1"
|
||||
}
|
||||
}
|
||||
77
examples/counters/e2e/playwright.config.ts
Normal file
77
examples/counters/e2e/playwright.config.ts
Normal 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.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["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,
|
||||
// },
|
||||
});
|
||||
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add 1000 Counters", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
|
||||
await Promise.all([
|
||||
await ui.goto(),
|
||||
await ui.addOneThousandCountersButton.waitFor(),
|
||||
]);
|
||||
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add Counter", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Clear Counters", () => {
|
||||
test("should reset the counts", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.clearCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Decrement Count", () => {
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
});
|
||||
});
|
||||
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class CountersPage {
|
||||
readonly page: Page;
|
||||
readonly addCounterButton: Locator;
|
||||
readonly addOneThousandCountersButton: Locator;
|
||||
readonly clearCountersButton: Locator;
|
||||
|
||||
readonly incrementCountButton: Locator;
|
||||
readonly counterInput: Locator;
|
||||
readonly decrementCountButton: Locator;
|
||||
readonly removeCountButton: Locator;
|
||||
|
||||
readonly total: Locator;
|
||||
readonly counters: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
|
||||
|
||||
this.addOneThousandCountersButton = page.locator("button", {
|
||||
hasText: "Add 1000 Counters",
|
||||
});
|
||||
|
||||
this.clearCountersButton = page.locator("button", {
|
||||
hasText: "Clear Counters",
|
||||
});
|
||||
|
||||
this.decrementCountButton = page.locator("button", {
|
||||
hasText: "-1",
|
||||
});
|
||||
|
||||
this.incrementCountButton = page.locator("button", {
|
||||
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() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async addCounter() {
|
||||
await Promise.all([
|
||||
this.addCounterButton.waitFor(),
|
||||
this.addCounterButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async addOneThousandCounters() {
|
||||
this.addOneThousandCountersButton.click();
|
||||
}
|
||||
|
||||
async decrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.decrementCountButton.nth(index).waitFor(),
|
||||
this.decrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async incrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.incrementCountButton.nth(index).waitFor(),
|
||||
this.incrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearCounters() {
|
||||
await Promise.all([
|
||||
this.clearCountersButton.waitFor(),
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Increment Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
});
|
||||
});
|
||||
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("View Counters", () => {
|
||||
test("should see the title", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(page).toHaveTitle("Counters");
|
||||
});
|
||||
|
||||
test("should see the initial counts", async ({ page }) => {
|
||||
const counters = new CountersPage(page);
|
||||
await counters.goto();
|
||||
|
||||
await expect(counters.total).toHaveText("0");
|
||||
await expect(counters.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
<title>Counters</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -44,12 +44,13 @@ pub fn Counters() -> impl IntoView {
|
||||
<button on:click=clear_counters>"Clear Counters"</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span>
|
||||
<span data-testid="total">
|
||||
{move || {
|
||||
counters.get().iter().map(|(_, count)| count.get()).sum::<i32>().to_string()
|
||||
}}
|
||||
|
||||
</span> " from " <span>{move || counters.get().len().to_string()}</span>
|
||||
</span> " from "
|
||||
<span data-testid="counters">{move || counters.get().len().to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
|
||||
@@ -23,7 +23,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>0</span> from <span>0</span> counters.</p><ul><!----></ul>"
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
|
||||
);
|
||||
|
||||
// add 3 counters
|
||||
@@ -38,7 +38,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>0</span> from <span>3</span> \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -80,7 +80,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>6</span> from <span>3</span> \
|
||||
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
@@ -105,7 +105,7 @@ async fn inc() {
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span>5</span> from <span>2</span> \
|
||||
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
|
||||
@@ -16,6 +16,7 @@ server_fn = { path = "../../server_fn", features = [
|
||||
"serde-lite",
|
||||
"rkyv",
|
||||
"multipart",
|
||||
"postcard",
|
||||
] }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
|
||||
@@ -6,7 +6,8 @@ use server_fn::{
|
||||
client::{browser::BrowserClient, Client},
|
||||
codec::{
|
||||
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
|
||||
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
|
||||
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
},
|
||||
request::{browser::BrowserRequest, ClientReq, Req},
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
@@ -65,6 +66,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
<h2>"Alternative Encodings"</h2>
|
||||
<ServerFnArgumentExample/>
|
||||
<RkyvExample/>
|
||||
<PostcardExample/>
|
||||
<FileUpload/>
|
||||
<FileUploadWithProgress/>
|
||||
<FileWatcher/>
|
||||
@@ -880,3 +882,67 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
})>Click me</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PostcardData {
|
||||
name: String,
|
||||
age: u32,
|
||||
hobbies: Vec<String>,
|
||||
}
|
||||
|
||||
/// This server function uses Postcard for both input and output encoding.
|
||||
/// Postcard provides efficient binary serialization, almost as fast as rkyv, while also being
|
||||
/// serde compatible
|
||||
#[server(input = Postcard, output = Postcard)]
|
||||
pub async fn postcard_example(
|
||||
data: PostcardData,
|
||||
) -> Result<PostcardData, ServerFnError> {
|
||||
// Simulate some processing time
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
|
||||
// Modify the data to demonstrate server-side changes
|
||||
let mut modified_data = data.clone();
|
||||
modified_data.age += 1;
|
||||
modified_data.hobbies.push("Rust programming".to_string());
|
||||
|
||||
Ok(modified_data)
|
||||
}
|
||||
|
||||
/// This component demonstrates the usage of Postcard encoding with server functions.
|
||||
/// It allows incrementing the age of a person and shows how the data is
|
||||
/// serialized, sent to the server, processed, and returned.
|
||||
#[component]
|
||||
pub fn PostcardExample() -> impl IntoView {
|
||||
// Initialize the input data
|
||||
let (input, set_input) = signal(PostcardData {
|
||||
name: "Alice".to_string(),
|
||||
age: 30,
|
||||
hobbies: vec!["reading".to_string(), "hiking".to_string()],
|
||||
});
|
||||
|
||||
// Create a resource that will call the server function whenever the input changes
|
||||
let postcard_result = Resource::new(
|
||||
move || input.get(),
|
||||
|data| async move { postcard_example(data).await },
|
||||
);
|
||||
|
||||
view! {
|
||||
<h3>Using <code>postcard</code> encoding</h3>
|
||||
<p>"This example demonstrates using Postcard for efficient binary serialization."</p>
|
||||
<button on:click=move |_| {
|
||||
// Update the input data when the button is clicked
|
||||
set_input.update(|data| {
|
||||
data.age += 1;
|
||||
});
|
||||
}>
|
||||
"Increment Age"
|
||||
</button>
|
||||
// Display the current input data
|
||||
<p>"Input: " {move || format!("{:?}", input.get())}</p>
|
||||
<Transition>
|
||||
// Display the result from the server, which will update automatically
|
||||
// when the input changes due to the resource
|
||||
<p>"Result: " {move || postcard_result.get().map(|r| format!("{:?}", r))}</p>
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
serde_json = "1.0"
|
||||
parking_lot = "0.12.3"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tokio = { version = "1.39", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
@@ -31,3 +31,7 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -207,7 +207,10 @@ impl ExtendResponse for ActixResponse {
|
||||
/// without actually setting the status code. This means that the client will not follow the
|
||||
/// redirect, and can therefore return the value of the server function and then handle
|
||||
/// the redirect with client-side routing.
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn redirect(path: &str) {
|
||||
if let (Some(req), Some(res)) =
|
||||
(use_context::<Request>(), use_context::<ResponseOptions>())
|
||||
@@ -239,10 +242,14 @@ pub fn redirect(path: &str) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Couldn't retrieve either Parts or ResponseOptions while trying \
|
||||
to redirect()."
|
||||
);
|
||||
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
|
||||
trying to redirect().";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", &msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", &msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +289,10 @@ pub fn redirect(path: &str) {
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn handle_server_fns() -> Route {
|
||||
handle_server_fns_with_context(|| {})
|
||||
}
|
||||
@@ -306,7 +316,10 @@ pub fn handle_server_fns() -> Route {
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [ResponseOptions]
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
) -> Route {
|
||||
@@ -438,7 +451,10 @@ pub fn handle_server_fns_with_context(
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
method: Method,
|
||||
@@ -505,7 +521,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
method: Method,
|
||||
@@ -567,7 +586,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
method: Method,
|
||||
@@ -590,7 +612,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -624,7 +649,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -655,7 +683,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -685,7 +716,10 @@ where
|
||||
/// - [HttpRequest](actix_web::HttpRequest)
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -704,7 +738,10 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn provide_contexts(
|
||||
req: Request,
|
||||
meta_context: &ServerMetaContext,
|
||||
@@ -1205,7 +1242,10 @@ where
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1217,7 +1257,10 @@ where
|
||||
self.leptos_routes_with_context(paths, || {}, app_fn)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1304,7 +1347,10 @@ where
|
||||
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl LeptosRoutes for &mut ServiceConfig {
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
@@ -1316,7 +1362,10 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
self.leptos_routes_with_context(paths, || {}, app_fn)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
paths: Vec<ActixRouteListing>,
|
||||
|
||||
@@ -28,7 +28,7 @@ serde_json = "1.0"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
tower = "0.4.13"
|
||||
tower-http = "0.5.2"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7.5"
|
||||
@@ -38,6 +38,10 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
|
||||
wasm = []
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs"]
|
||||
islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -236,10 +236,14 @@ pub fn redirect(path: &str) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Couldn't retrieve either Parts or ResponseOptions while trying \
|
||||
to redirect()."
|
||||
);
|
||||
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
|
||||
trying to redirect().";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", &msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", &msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +293,10 @@ pub fn generate_request_and_parts(
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub async fn handle_server_fns(req: Request<Body>) -> impl IntoResponse {
|
||||
handle_server_fns_inner(|| {}, req).await
|
||||
}
|
||||
@@ -331,7 +338,10 @@ fn init_executor() {
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub async fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
@@ -460,7 +470,10 @@ pub type PinnedHtmlStream =
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
@@ -480,7 +493,10 @@ where
|
||||
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
||||
/// one respects the `SsrMode` on each Route and thus requires `Vec<AxumRouteListing>` for route checking.
|
||||
/// This is useful if you are using `.leptos_routes_with_handler()`
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_route<IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -546,7 +562,10 @@ where
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
@@ -597,7 +616,10 @@ where
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -622,7 +644,10 @@ where
|
||||
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
||||
/// one respects the `SsrMode` on each Route, and thus requires `Vec<AxumRouteListing>` for route checking.
|
||||
/// This is useful if you are using `.leptos_routes_with_handler()`.
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_route_with_context<IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -701,7 +726,10 @@ where
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -766,7 +794,10 @@ where
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -859,7 +890,10 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn provide_contexts(
|
||||
path: &str,
|
||||
meta_context: &ServerMetaContext,
|
||||
@@ -924,7 +958,10 @@ fn provide_contexts(
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
@@ -976,7 +1013,10 @@ where
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -1041,7 +1081,10 @@ where
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`ServerMetaContext`](leptos_meta::ServerMetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
@@ -1072,7 +1115,10 @@ where
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
) -> Vec<AxumRouteListing>
|
||||
@@ -1085,7 +1131,10 @@ where
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||
@@ -1099,7 +1148,10 @@ where
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
|
||||
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_exclusions<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
@@ -1140,7 +1192,10 @@ pub async fn build_static_routes<IV>(
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
|
||||
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
@@ -1227,7 +1282,10 @@ impl AxumRouteListing {
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
|
||||
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
|
||||
/// Additional context will be provided to the app Element.
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||
excluded_routes: Option<Vec<String>>,
|
||||
@@ -1554,7 +1612,10 @@ impl<S> LeptosRoutes<S> for axum::Router<S>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
state: &S,
|
||||
@@ -1567,7 +1628,10 @@ where
|
||||
self.leptos_routes_with_context(state, paths, || {}, app_fn)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes_with_context<IV>(
|
||||
self,
|
||||
state: &S,
|
||||
@@ -1710,7 +1774,10 @@ where
|
||||
router
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
|
||||
@@ -16,7 +16,6 @@ leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["sandboxed-arenas"] }
|
||||
tracing = "0.1.40"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -30,7 +30,7 @@ reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
typed-builder = "0.19.1"
|
||||
typed-builder-macro = "0.19.1"
|
||||
serde = "1.0"
|
||||
@@ -76,13 +76,20 @@ ssr = [
|
||||
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
|
||||
rkyv = ["server_fn/rkyv"]
|
||||
tracing = [
|
||||
"dep:tracing",
|
||||
"reactive_graph/tracing",
|
||||
"tachys/tracing",
|
||||
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
|
||||
"leptos_macro/tracing",
|
||||
"leptos_dom/tracing",
|
||||
"leptos_server/tracing",
|
||||
]
|
||||
nonce = ["base64", "rand"]
|
||||
spin = ["leptos-spin-macro"]
|
||||
experimental-islands = ["leptos_macro/experimental-islands", "dep:serde_json"]
|
||||
trace-component-props = ["leptos_macro/trace-component-props"]
|
||||
trace-component-props = [
|
||||
"leptos_macro/trace-component-props",
|
||||
"leptos_dom/trace-component-props"
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -93,52 +100,22 @@ denylist = [
|
||||
"rustls",
|
||||
"default-tls",
|
||||
"wasm-bindgen",
|
||||
"rkyv", # was causing clippy issues on nightly
|
||||
"rkyv", # was causing clippy issues on nightly
|
||||
"trace-component-props",
|
||||
"spin",
|
||||
"experimental-islands",
|
||||
]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"miniserde",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"rkyv",
|
||||
],
|
||||
[
|
||||
"default-tls",
|
||||
"rustls",
|
||||
],
|
||||
["csr", "ssr"],
|
||||
["csr", "hydrate"],
|
||||
["ssr", "hydrate"],
|
||||
["serde", "serde-lite"],
|
||||
["serde-lite", "miniserde"],
|
||||
["serde", "miniserde"],
|
||||
["serde", "rkyv"],
|
||||
["miniserde", "rkyv"],
|
||||
["serde-lite", "rkyv"],
|
||||
["default-tls", "rustls"],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -43,10 +43,7 @@ use leptos_reactive::{
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn AnimatedShow(
|
||||
/// The components Show wraps
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn MyComponent(
|
||||
//! #[prop(into)] render_number: Callback<i32, String>,
|
||||
//! #[prop(into)] render_number: Callback<(i32,), String>,
|
||||
//! ) -> impl IntoView {
|
||||
//! view! {
|
||||
//! <div>
|
||||
//! {render_number.run(1)}
|
||||
//! {render_number.run((1,))}
|
||||
//! // callbacks can be called multiple times
|
||||
//! {render_number.run(42)}
|
||||
//! {render_number.run((42,))}
|
||||
//! </div>
|
||||
//! }
|
||||
//! }
|
||||
@@ -85,17 +85,39 @@ impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, In, T, Out> From<F> for UnsyncCallback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> T + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
In: 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(move |x| f(x).into())
|
||||
}
|
||||
macro_rules! impl_unsync_callable_from_fn {
|
||||
($($arg:ident),*) => {
|
||||
impl<F, $($arg,)* T, Out> From<F> for UnsyncCallback<($($arg,)*), Out>
|
||||
where
|
||||
F: Fn($($arg),*) -> T + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
$($arg: 'static,)*
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
paste::paste!(
|
||||
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_unsync_callable_from_fn!();
|
||||
impl_unsync_callable_from_fn!(P1);
|
||||
impl_unsync_callable_from_fn!(P1, P2);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
|
||||
impl_unsync_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
|
||||
impl_unsync_callable_from_fn!(
|
||||
P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12
|
||||
);
|
||||
|
||||
/// Callbacks define a standard way to store functions and closures.
|
||||
///
|
||||
/// # Example
|
||||
@@ -104,11 +126,11 @@ where
|
||||
/// # use leptos::callback::{Callable, Callback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: Callback<i32, String>,
|
||||
/// #[prop(into)] render_number: Callback<(i32,), String>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// {render_number.run(42)}
|
||||
/// {render_number.run((42,))}
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
@@ -148,17 +170,37 @@ impl<In, Out> Clone for Callback<In, Out> {
|
||||
|
||||
impl<In, Out> Copy for Callback<In, Out> {}
|
||||
|
||||
impl<F, In, T, Out> From<F> for Callback<In, Out>
|
||||
where
|
||||
F: Fn(In) -> T + Send + Sync + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
In: Send + Sync + 'static,
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
Self::new(move |x| f(x).into())
|
||||
}
|
||||
macro_rules! impl_callable_from_fn {
|
||||
($($arg:ident),*) => {
|
||||
impl<F, $($arg,)* T, Out> From<F> for Callback<($($arg,)*), Out>
|
||||
where
|
||||
F: Fn($($arg),*) -> T + Send + Sync + 'static,
|
||||
T: Into<Out> + 'static,
|
||||
$($arg: Send + Sync + 'static,)*
|
||||
{
|
||||
fn from(f: F) -> Self {
|
||||
paste::paste!(
|
||||
Self::new(move |($([<$arg:lower>],)*)| f($([<$arg:lower>]),*).into())
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_callable_from_fn!();
|
||||
impl_callable_from_fn!(P1);
|
||||
impl_callable_from_fn!(P1, P2);
|
||||
impl_callable_from_fn!(P1, P2, P3);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11);
|
||||
impl_callable_from_fn!(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12);
|
||||
|
||||
impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(fun: F) -> Self
|
||||
@@ -190,11 +232,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn runback_from() {
|
||||
let _callback: Callback<(), String> = (|()| "test").into();
|
||||
let _callback: Callback<(), String> = (|| "test").into();
|
||||
let _callback: Callback<(i32, String), String> =
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from() {
|
||||
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
|
||||
let _callback: UnsyncCallback<(), String> = (|| "test").into();
|
||||
let _callback: UnsyncCallback<(i32, String), String> =
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,7 @@ use web_sys::{
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn>(
|
||||
/// The action from which to build the form. This should include a URL, which can be generated
|
||||
|
||||
@@ -302,6 +302,9 @@ pub use serde;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
#[doc(hidden)]
|
||||
pub use serde_json;
|
||||
#[cfg(feature = "tracing")]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
#[doc(hidden)]
|
||||
pub use wasm_bindgen;
|
||||
#[doc(hidden)]
|
||||
@@ -376,7 +379,7 @@ pub use show::*;
|
||||
//pub use suspense_component::*;
|
||||
mod suspense_component;
|
||||
//mod transition;
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
#[cfg(feature = "tracing")]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
|
||||
@@ -10,10 +10,7 @@ use std::sync::Arc;
|
||||
/// If no mount point is given, the portal is inserted in `document.body`;
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
|
||||
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn Portal<V>(
|
||||
/// Target element where the children will be appended
|
||||
|
||||
@@ -13,7 +13,7 @@ use reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{Get, Read, Track, With},
|
||||
traits::{Dispose, Get, Read, Track, With},
|
||||
};
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use tachys::{
|
||||
@@ -277,7 +277,7 @@ where
|
||||
futures::channel::oneshot::channel::<()>();
|
||||
|
||||
let mut tasks_tx = Some(tasks_tx);
|
||||
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
|
||||
let eff = reactive_graph::effect::Effect::new_isomorphic({
|
||||
move |_| {
|
||||
tasks.track();
|
||||
if tasks.read().is_empty() {
|
||||
@@ -337,7 +337,7 @@ where
|
||||
}
|
||||
children = children => {
|
||||
// clean up the (now useless) effect
|
||||
drop(eff);
|
||||
eff.dispose();
|
||||
|
||||
Some(OwnedView::new_with_owner(children, owner))
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ reactive_graph = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.69"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
@@ -26,7 +28,11 @@ features = ["Location"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tracing = []
|
||||
tracing = ["dep:tracing"]
|
||||
trace-component-props = ["dep:serde", "dep:serde_json"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
//! DOM helpers for Leptos.
|
||||
|
||||
pub mod helpers;
|
||||
#[doc(hidden)]
|
||||
pub mod macro_helpers;
|
||||
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
#[macro_use]
|
||||
|
||||
3
leptos_dom/src/macro_helpers/mod.rs
Normal file
3
leptos_dom/src/macro_helpers/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
#[cfg(feature = "trace-component-props")]
|
||||
#[doc(hidden)]
|
||||
pub mod tracing_property;
|
||||
170
leptos_dom/src/macro_helpers/tracing_property.rs
Normal file
170
leptos_dom/src/macro_helpers/tracing_property.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
#[macro_export]
|
||||
/// Use for tracing property
|
||||
macro_rules! tracing_props {
|
||||
() => {
|
||||
::leptos::tracing::span!(
|
||||
::leptos::tracing::Level::TRACE,
|
||||
"leptos_dom::tracing_props",
|
||||
props = String::from("[]")
|
||||
);
|
||||
};
|
||||
($($prop:tt),+ $(,)?) => {
|
||||
{
|
||||
use ::leptos::leptos_dom::macro_helpers::tracing_property::{Match, SerializeMatch, DefaultMatch};
|
||||
let mut props = String::from('[');
|
||||
$(
|
||||
let prop = (&&Match {
|
||||
name: stringify!{$prop},
|
||||
value: std::cell::Cell::new(Some(&$prop))
|
||||
}).spez();
|
||||
props.push_str(&format!("{prop},"));
|
||||
)*
|
||||
props.pop();
|
||||
props.push(']');
|
||||
::leptos::tracing::span!(
|
||||
::leptos::tracing::Level::TRACE,
|
||||
"leptos_dom::tracing_props",
|
||||
props
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation based on spez
|
||||
// see https://github.com/m-ou-se/spez
|
||||
|
||||
pub struct Match<T> {
|
||||
pub name: &'static str,
|
||||
pub value: std::cell::Cell<Option<T>>,
|
||||
}
|
||||
|
||||
pub trait SerializeMatch {
|
||||
type Return;
|
||||
fn spez(&self) -> Self::Return;
|
||||
}
|
||||
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
|
||||
// suppresses warnings when serializing signals into props
|
||||
#[cfg(debug_assertions)]
|
||||
let _z = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
|
||||
serde_json::to_string(self.value.get().unwrap_throw()).map_or_else(
|
||||
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|
||||
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DefaultMatch {
|
||||
type Return;
|
||||
fn spez(&self) -> Self::Return;
|
||||
}
|
||||
impl<T> DefaultMatch for Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_primitive() {
|
||||
// String
|
||||
let test = String::from("string");
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
|
||||
|
||||
// &str
|
||||
let test = "string";
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
|
||||
|
||||
// u128
|
||||
let test: u128 = 1;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 1}"#);
|
||||
|
||||
// i128
|
||||
let test: i128 = -1;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
|
||||
|
||||
// f64
|
||||
let test = 3.25;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
|
||||
|
||||
// bool
|
||||
let test = true;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": true}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_serialize() {
|
||||
use serde::Serialize;
|
||||
#[derive(Serialize)]
|
||||
struct CustomStruct {
|
||||
field: &'static str,
|
||||
}
|
||||
|
||||
let test = CustomStruct { field: "field" };
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::needless_borrow)]
|
||||
fn match_no_serialize() {
|
||||
struct CustomStruct {
|
||||
field: &'static str,
|
||||
}
|
||||
|
||||
let test = CustomStruct { field: "field" };
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(
|
||||
prop,
|
||||
r#"{"name": "test", "value": "[unserializable value]"}"#
|
||||
);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
}
|
||||
@@ -27,7 +27,7 @@ leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4.22"
|
||||
@@ -43,7 +43,7 @@ csr = []
|
||||
hydrate = []
|
||||
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
tracing = ["dep:tracing"]
|
||||
experimental-islands = []
|
||||
trace-component-props = []
|
||||
actix = ["server_fn_macro/actix"]
|
||||
|
||||
@@ -142,6 +142,7 @@ impl ToTokens for Model {
|
||||
let props_name = format_ident!("{name}Props");
|
||||
let props_builder_name = format_ident!("{name}PropsBuilder");
|
||||
let props_serialized_name = format_ident!("{name}PropsSerialized");
|
||||
#[cfg(feature = "tracing")]
|
||||
let trace_name = format!("<{name} />");
|
||||
|
||||
let is_island_with_children = *is_island
|
||||
@@ -190,32 +191,38 @@ impl ToTokens for Model {
|
||||
tracing_span_expr,
|
||||
tracing_guard_expr,
|
||||
tracing_props_expr,
|
||||
) = if cfg!(feature = "tracing") {
|
||||
(
|
||||
quote! {
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
let span = ::leptos::tracing::Span::current();
|
||||
},
|
||||
quote! {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
if no_props {
|
||||
quote! {}
|
||||
} else {
|
||||
) = {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
(
|
||||
quote! {
|
||||
::leptos::tracing_props![#prop_names];
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(quote! {}, quote! {}, quote! {}, quote! {})
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
let span = ::leptos::tracing::Span::current();
|
||||
},
|
||||
quote! {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
if no_props || !cfg!(feature = "trace-component-props") {
|
||||
quote!()
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::leptos_dom::tracing_props![#prop_names];
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
{
|
||||
(quote!(), quote!(), quote!(), quote!())
|
||||
}
|
||||
};
|
||||
|
||||
let component_id = name.to_string();
|
||||
|
||||
@@ -13,6 +13,7 @@ use component::DummyModel;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenTree};
|
||||
use quote::{quote, ToTokens};
|
||||
use std::str::FromStr;
|
||||
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
|
||||
|
||||
mod params;
|
||||
@@ -260,10 +261,7 @@ mod slot;
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
let tokens: proc_macro2::TokenStream = tokens.into();
|
||||
let mut tokens = tokens.into_iter();
|
||||
@@ -331,6 +329,31 @@ fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// This behaves like the [`view`] macro, but loads the view from an external file instead of
|
||||
/// parsing it inline.
|
||||
///
|
||||
/// This is designed to allow editing views in a separate file, if this improves a user's workflow.
|
||||
///
|
||||
/// The file is loaded and parsed during proc-macro execution, and its path is resolved relative to
|
||||
/// the crate root rather than relative to the file from which it is called.
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
let file_name = syn::parse::<syn::LitStr>(tokens).unwrap_or_else(|_| {
|
||||
abort!(
|
||||
Span::call_site(),
|
||||
"the only supported argument is a string literal"
|
||||
);
|
||||
});
|
||||
let file =
|
||||
std::fs::read_to_string(file_name.value()).unwrap_or_else(|_| {
|
||||
abort!(Span::call_site(), "could not open file");
|
||||
});
|
||||
let tokens = proc_macro2::TokenStream::from_str(&file)
|
||||
.unwrap_or_else(|e| abort!(Span::call_site(), e));
|
||||
view(tokens.into())
|
||||
}
|
||||
|
||||
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
|
||||
///
|
||||
/// The `#[component]` macro allows you to annotate plain Rust functions as components
|
||||
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
codee = { version = "0.1.2", features = ["json_serde"] }
|
||||
codee = { version = "0.2.0", features = ["json_serde"] }
|
||||
hydration_context = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["hydration"] }
|
||||
server_fn = { workspace = true }
|
||||
@@ -38,7 +38,7 @@ tachys = ["dep:tachys"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
denylist = ["tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -95,8 +95,8 @@ where
|
||||
{
|
||||
/// Calls the `async` function with a reference to the input type as its argument.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn dispatch(&self, input: I) {
|
||||
self.0.with_value(|a| a.dispatch(input))
|
||||
@@ -104,8 +104,8 @@ where
|
||||
|
||||
/// The set of all submissions to this multi-action.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
|
||||
self.0.with_value(|a| a.submissions())
|
||||
@@ -119,8 +119,8 @@ where
|
||||
|
||||
/// How many times an action has successfully resolved.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn version(&self) -> RwSignal<usize> {
|
||||
self.0.with_value(|a| a.version)
|
||||
@@ -129,8 +129,8 @@ where
|
||||
/// Associates the URL of the given server function with this action.
|
||||
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn using_server_fn<T: ServerFn>(self) -> Self {
|
||||
self.0.update_value(|a| {
|
||||
@@ -201,8 +201,8 @@ where
|
||||
{
|
||||
/// Calls the `async` function with a reference to the input type as its argument.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn dispatch(&self, input: I) {
|
||||
if !is_suppressing_resource_load() {
|
||||
@@ -293,10 +293,7 @@ where
|
||||
/// create_multi_action(|input: &(usize, String)| async { todo!() });
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
pub fn create_multi_action<I, O, F, Fu>(action_fn: F) -> MultiAction<I, O>
|
||||
where
|
||||
I: 'static,
|
||||
@@ -333,10 +330,7 @@ where
|
||||
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>();
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
pub fn create_server_multi_action<S>(
|
||||
) -> MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
where
|
||||
|
||||
@@ -129,6 +129,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.data.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
|
||||
/// Re-runs the async function with the current source data.
|
||||
pub fn refetch(&self) {
|
||||
*self.refetch.write() += 1;
|
||||
@@ -172,6 +180,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> ArcResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcResource<T, JsonSerdeCodec>
|
||||
where
|
||||
JsonSerdeCodec: Encoder<T> + Decoder<T>,
|
||||
@@ -675,12 +701,36 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
self.data
|
||||
.try_with(|n| n.as_ref().map(|n| Some(f(n))))?
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Re-runs the async function with the current source data.
|
||||
pub fn refetch(&self) {
|
||||
self.refetch.try_update(|n| *n += 1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> Resource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync,
|
||||
E: Send + Sync + Clone,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> IntoFuture for Resource<T, Ser>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
|
||||
@@ -14,7 +14,7 @@ once_cell = "1.19"
|
||||
or_poisoned = { workspace = true }
|
||||
indexmap = "2.3"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = "0.1.40"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
futures = "0.3.30"
|
||||
|
||||
@@ -25,6 +25,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
[features]
|
||||
default = []
|
||||
ssr = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -342,10 +342,14 @@ where
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"tried to use a leptos_meta component without `ServerMetaContext` \
|
||||
provided"
|
||||
);
|
||||
let msg = "tried to use a leptos_meta component without \
|
||||
`ServerMetaContext` provided";
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
|
||||
RegisteredMetaTag { el }
|
||||
|
||||
@@ -44,3 +44,6 @@ sandboxed-arenas = []
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -163,10 +163,10 @@ where
|
||||
#[deprecated = "This function is being removed to conform to Rust idioms. \
|
||||
Please use `Selector::new()` instead."]
|
||||
pub fn create_selector<T>(
|
||||
source: impl Fn() -> T + Clone + 'static,
|
||||
source: impl Fn() -> T + Clone + Send + Sync + 'static,
|
||||
) -> Selector<T>
|
||||
where
|
||||
T: PartialEq + Eq + Clone + std::hash::Hash + 'static,
|
||||
T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
|
||||
{
|
||||
Selector::new(source)
|
||||
}
|
||||
@@ -178,11 +178,11 @@ where
|
||||
#[deprecated = "This function is being removed to conform to Rust idioms. \
|
||||
Please use `Selector::new_with_fn()` instead."]
|
||||
pub fn create_selector_with_fn<T>(
|
||||
source: impl Fn() -> T + Clone + 'static,
|
||||
source: impl Fn() -> T + Clone + Send + Sync + 'static,
|
||||
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
|
||||
) -> Selector<T>
|
||||
where
|
||||
T: PartialEq + Eq + Clone + std::hash::Hash + 'static,
|
||||
T: PartialEq + Eq + Send + Sync + Clone + std::hash::Hash + 'static,
|
||||
{
|
||||
Selector::new_with_fn(source, f)
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ where
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn new(fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static) -> Self
|
||||
where
|
||||
@@ -127,7 +127,7 @@ where
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn new_with_compare(
|
||||
fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static,
|
||||
@@ -153,7 +153,7 @@ where
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn new_owning(
|
||||
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
|
||||
|
||||
@@ -149,7 +149,7 @@ where
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "debug", skip_all,)
|
||||
tracing::instrument(level = "debug", skip_all)
|
||||
)]
|
||||
/// Creates a new memoized, computed reactive value.
|
||||
///
|
||||
@@ -184,7 +184,7 @@ where
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
/// Creates a new memo with a custom comparison function. By default, memos simply use
|
||||
/// [`PartialEq`] to compare the previous value to the new value. Passing a custom comparator
|
||||
@@ -218,7 +218,7 @@ where
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
pub fn new_owning(
|
||||
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
|
||||
|
||||
@@ -30,7 +30,7 @@ use std::{
|
||||
/// let a = RwSignal::new(0);
|
||||
/// let is_selected = Selector::new(move || a.get());
|
||||
/// let total_notifications = StoredValue::new(0);
|
||||
/// Effect::new({
|
||||
/// Effect::new_isomorphic({
|
||||
/// let is_selected = is_selected.clone();
|
||||
/// move |_| {
|
||||
/// if is_selected.selected(5) {
|
||||
@@ -55,7 +55,7 @@ use std::{
|
||||
///
|
||||
/// # any_spawner::Executor::tick().await;
|
||||
/// assert_eq!(is_selected.selected(5), false);
|
||||
/// # });
|
||||
/// # }).await;
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
@@ -74,17 +74,17 @@ where
|
||||
|
||||
impl<T> Selector<T>
|
||||
where
|
||||
T: PartialEq + Eq + Clone + Hash + 'static,
|
||||
T: PartialEq + Send + Sync + Eq + Clone + Hash + 'static,
|
||||
{
|
||||
/// Creates a new selector that compares values using [`PartialEq`].
|
||||
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
|
||||
pub fn new(source: impl Fn() -> T + Send + Sync + Clone + 'static) -> Self {
|
||||
Self::new_with_fn(source, PartialEq::eq)
|
||||
}
|
||||
|
||||
/// Creates a new selector that compares values by returning `true` from a comparator function
|
||||
/// if the values are the same.
|
||||
pub fn new_with_fn(
|
||||
source: impl Fn() -> T + Clone + 'static,
|
||||
source: impl Fn() -> T + Clone + Send + Sync + 'static,
|
||||
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
|
||||
) -> Self {
|
||||
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
|
||||
@@ -92,7 +92,7 @@ where
|
||||
let v: Arc<RwLock<Option<T>>> = Default::default();
|
||||
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
|
||||
|
||||
let effect = Arc::new(RenderEffect::new({
|
||||
let effect = Arc::new(RenderEffect::new_isomorphic({
|
||||
let subs = Arc::clone(&subs);
|
||||
let f = Arc::clone(&f);
|
||||
let v = Arc::clone(&v);
|
||||
|
||||
@@ -43,6 +43,7 @@ use std::{
|
||||
/// # use reactive_graph::owner::StoredValue;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # tokio::task::LocalSet::new().run_until(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// let a = RwSignal::new(0);
|
||||
/// let b = RwSignal::new(0);
|
||||
///
|
||||
@@ -52,7 +53,9 @@ use std::{
|
||||
/// println!("Value: {}", a.get());
|
||||
/// });
|
||||
///
|
||||
/// # assert_eq!(a.get(), 0);
|
||||
/// a.set(1);
|
||||
/// # assert_eq!(a.get(), 1);
|
||||
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
|
||||
///
|
||||
/// // ❌ don't use effects to synchronize state within the reactive system
|
||||
@@ -61,7 +64,7 @@ use std::{
|
||||
/// // and easily lead to problems like infinite loops
|
||||
/// b.set(a.get() + 1);
|
||||
/// });
|
||||
/// # });
|
||||
/// # }).await;
|
||||
/// # });
|
||||
/// ```
|
||||
/// ## Web-Specific Notes
|
||||
@@ -182,6 +185,7 @@ impl Effect<LocalStorage> {
|
||||
/// # use reactive_graph::signal::signal;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # tokio::task::LocalSet::new().run_until(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// #
|
||||
/// let (num, set_num) = signal(0);
|
||||
///
|
||||
@@ -192,13 +196,16 @@ impl Effect<LocalStorage> {
|
||||
/// },
|
||||
/// false,
|
||||
/// );
|
||||
/// # assert_eq!(num.get(), 0);
|
||||
///
|
||||
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
|
||||
/// # assert_eq!(num.get(), 1);
|
||||
///
|
||||
/// effect.stop(); // stop watching
|
||||
///
|
||||
/// set_num.set(2); // (nothing happens)
|
||||
/// # });
|
||||
/// # assert_eq!(num.get(), 2);
|
||||
/// # }).await;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
@@ -210,6 +217,7 @@ impl Effect<LocalStorage> {
|
||||
/// # use reactive_graph::signal::signal;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # tokio::task::LocalSet::new().run_until(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// #
|
||||
/// let (num, set_num) = signal(0);
|
||||
/// let (cb_num, set_cb_num) = signal(0);
|
||||
@@ -222,12 +230,17 @@ impl Effect<LocalStorage> {
|
||||
/// false,
|
||||
/// );
|
||||
///
|
||||
/// # assert_eq!(num.get(), 0);
|
||||
/// set_num.set(1); // > "Number: 1; Cb: 0"
|
||||
/// # assert_eq!(num.get(), 1);
|
||||
///
|
||||
/// # assert_eq!(cb_num.get(), 0);
|
||||
/// set_cb_num.set(1); // (nothing happens)
|
||||
/// # assert_eq!(cb_num.get(), 1);
|
||||
///
|
||||
/// set_num.set(2); // > "Number: 2; Cb: 1"
|
||||
/// # });
|
||||
/// # assert_eq!(num.get(), 2);
|
||||
/// # }).await;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
@@ -243,6 +256,7 @@ impl Effect<LocalStorage> {
|
||||
/// # use reactive_graph::signal::signal;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # tokio::task::LocalSet::new().run_until(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// #
|
||||
/// let (num, set_num) = signal(0);
|
||||
///
|
||||
@@ -254,8 +268,10 @@ impl Effect<LocalStorage> {
|
||||
/// true,
|
||||
/// ); // > "Number: 0; Prev: None"
|
||||
///
|
||||
/// # assert_eq!(num.get(), 0);
|
||||
/// set_num.set(1); // > "Number: 1; Prev: Some(0)"
|
||||
/// # });
|
||||
/// # assert_eq!(num.get(), 1);
|
||||
/// # }).await;
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn watch<D, T>(
|
||||
|
||||
@@ -135,44 +135,50 @@ where
|
||||
{
|
||||
/// Creates a render effect that will run whether the `effects` feature is enabled or not.
|
||||
pub fn new_isomorphic(
|
||||
mut fun: impl FnMut(Option<T>) -> T + Send + 'static,
|
||||
fun: impl FnMut(Option<T>) -> T + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let (mut observer, mut rx) = channel();
|
||||
observer.notify();
|
||||
fn erased<T: Send + Sync + 'static>(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + Send + Sync + 'static>,
|
||||
) -> RenderEffect<T> {
|
||||
let (observer, mut rx) = channel();
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
let owner = Owner::new();
|
||||
let inner = Arc::new(RwLock::new(EffectInner {
|
||||
dirty: false,
|
||||
observer,
|
||||
sources: SourceSet::new(),
|
||||
}));
|
||||
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
let owner = Owner::new();
|
||||
let inner = Arc::new(RwLock::new(EffectInner {
|
||||
dirty: false,
|
||||
observer,
|
||||
sources: SourceSet::new(),
|
||||
}));
|
||||
let mut first_run = true;
|
||||
let initial_value = owner
|
||||
.with(|| inner.to_any_subscriber().with_observer(|| fun(None)));
|
||||
*value.write().or_poisoned() = Some(initial_value);
|
||||
|
||||
Executor::spawn({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
Executor::spawn({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if first_run
|
||||
|| subscriber
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if subscriber
|
||||
.with_observer(|| subscriber.update_if_necessary())
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
RenderEffect { value, inner }
|
||||
});
|
||||
|
||||
RenderEffect { value, inner }
|
||||
}
|
||||
|
||||
erased(Box::new(fun))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ impl<T> ArcRwSignal<T> {
|
||||
/// Creates a new signal, taking the initial value as its argument.
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn new(value: T) -> Self {
|
||||
|
||||
@@ -118,7 +118,7 @@ where
|
||||
/// Creates a new signal, taking the initial value as its argument.
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn new(value: T) -> Self {
|
||||
@@ -134,7 +134,7 @@ where
|
||||
/// Creates a new signal with the given arena storage method.
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn new_with_storage(value: T) -> Self {
|
||||
@@ -154,7 +154,7 @@ where
|
||||
/// this pins the value to the current thread. Accessing it from any other thread will panic.
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn new_local(value: T) -> Self {
|
||||
|
||||
@@ -316,7 +316,7 @@ mod tests {
|
||||
assert_eq!(store.user().read_untracked().as_str(), "Bob");
|
||||
Effect::new_sync({
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move |prev| {
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("first run");
|
||||
} else {
|
||||
@@ -360,7 +360,7 @@ mod tests {
|
||||
|
||||
Effect::new_sync({
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move |prev| {
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("first run");
|
||||
} else {
|
||||
@@ -392,7 +392,7 @@ mod tests {
|
||||
|
||||
Effect::new_sync({
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move |prev| {
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("first run");
|
||||
} else {
|
||||
@@ -429,7 +429,7 @@ mod tests {
|
||||
|
||||
Effect::new_sync({
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move |prev| {
|
||||
move |prev: Option<()>| {
|
||||
if prev.is_none() {
|
||||
println!("first run");
|
||||
} else {
|
||||
|
||||
@@ -64,3 +64,6 @@ nightly = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -47,6 +47,7 @@ serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
|
||||
futures = "0.3.30"
|
||||
http = { version = "1.1" }
|
||||
ciborium = { version = "0.2.2", optional = true }
|
||||
postcard = { version = "1", features = ["alloc"], optional = true }
|
||||
hyper = { version = "1.4", optional = true }
|
||||
bytes = "1.7"
|
||||
http-body-util = { version = "0.1.2", optional = true }
|
||||
@@ -108,6 +109,7 @@ url = ["dep:serde_qs"]
|
||||
cbor = ["dep:ciborium"]
|
||||
rkyv = ["dep:rkyv"]
|
||||
msgpack = ["dep:rmp-serde"]
|
||||
postcard = ["dep:postcard"]
|
||||
default-tls = ["reqwest?/default-tls"]
|
||||
rustls = ["reqwest?/rustls-tls"]
|
||||
reqwest = ["dep:reqwest"]
|
||||
@@ -196,4 +198,24 @@ skip_feature_sets = [
|
||||
"url",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"postcard",
|
||||
"json",
|
||||
],
|
||||
[
|
||||
"postcard",
|
||||
"cbor",
|
||||
],
|
||||
[
|
||||
"postcard",
|
||||
"url",
|
||||
],
|
||||
[
|
||||
"postcard",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"postcard",
|
||||
"rkyv",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -49,6 +49,11 @@ mod msgpack;
|
||||
#[cfg(feature = "msgpack")]
|
||||
pub use msgpack::*;
|
||||
|
||||
#[cfg(feature = "postcard")]
|
||||
mod postcard;
|
||||
#[cfg(feature = "postcard")]
|
||||
pub use postcard::*;
|
||||
|
||||
mod stream;
|
||||
use crate::error::ServerFnError;
|
||||
use futures::Future;
|
||||
|
||||
74
server_fn/src/codec/postcard.rs
Normal file
74
server_fn/src/codec/postcard.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use super::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
|
||||
use crate::{
|
||||
error::ServerFnError,
|
||||
request::{ClientReq, Req},
|
||||
response::{ClientRes, Res},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use http::Method;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// A codec for Postcard.
|
||||
pub struct Postcard;
|
||||
|
||||
impl Encoding for Postcard {
|
||||
const CONTENT_TYPE: &'static str = "application/x-postcard";
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
impl<T, Request, Err> IntoReq<Postcard, Request, Err> for T
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = postcard::to_allocvec(&self)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Request::try_new_post_bytes(
|
||||
path,
|
||||
Postcard::CONTENT_TYPE,
|
||||
accepts,
|
||||
Bytes::from(data),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Request, Err> FromReq<Postcard, Request, Err> for T
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
let data = req.try_into_bytes().await?;
|
||||
postcard::from_bytes::<T>(&data)
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> IntoRes<Postcard, Response, Err> for T
|
||||
where
|
||||
Response: Res<Err>,
|
||||
T: Serialize + Send,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
|
||||
let data = postcard::to_allocvec(&self)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Response::try_from_bytes(Postcard::CONTENT_TYPE, Bytes::from(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> FromRes<Postcard, Response, Err> for T
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
let data = res.try_into_bytes().await?;
|
||||
postcard::from_bytes(&data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -178,3 +178,6 @@ testing = ["dep:slotmap"]
|
||||
reactive_graph = ["dep:reactive_graph", "dep:any_spawner"]
|
||||
sledgehammer = ["dep:sledgehammer_bindgen", "dep:sledgehammer_utils"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
@@ -258,7 +258,7 @@ html_self_closing_elements! {
|
||||
/// The `<meta>` HTML element represents Metadata that cannot be represented by other HTML meta-related elements, like base, link, script, style or title.
|
||||
meta HtmlMetaElement [charset, content, http_equiv, name] true,
|
||||
/// The `<source>` HTML element specifies multiple media resources for the picture, the audio element, or the video element. It is an empty element, meaning that it has no content and does not have a closing tag. It is commonly used to offer the same media content in multiple file formats in order to provide compatibility with a broad range of browsers given their differing support for image file formats and media file formats.
|
||||
source HtmlSourceElement [src, r#type] true,
|
||||
source HtmlSourceElement [src, r#type, srcset, sizes, media, height, width] true,
|
||||
/// The `<track>` HTML element is used as a child of the media elements, audio and video. It lets you specify timed text tracks (or time-based data), for example to automatically handle subtitles. The tracks are formatted in WebVTT format (.vtt files) — Web Video Text Tracks.
|
||||
track HtmlTrackElement [default, kind, label, src, srclang] true,
|
||||
/// The `<wbr>` HTML element represents a word break opportunity—a position within text where the browser may optionally break a line, though its line-breaking rules would not otherwise create a break at that location.
|
||||
|
||||
@@ -562,7 +562,7 @@ mod tests {
|
||||
element::{em, ElementChild, Main},
|
||||
},
|
||||
renderer::mock_dom::MockDom,
|
||||
view::{Render, RenderHtml},
|
||||
view::Render,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user