Compare commits

...

28 Commits
2182 ... 2907

Author SHA1 Message Date
Greg Johnston
67ccd37336 fix: remove browser-only SendWrapper items during either kind of Suspense (closes #2907) 2024-09-02 07:57:03 -04:00
Greg Johnston
3b1b2e2dcc fix version for publish 2024-08-31 11:59:05 -04:00
Greg Johnston
7831e4ad05 next beta release 2024-08-31 11:59:05 -04:00
Greg Johnston
e7bb859cd9 feat: add support for static routing and incremental static regeneration (#2875) 2024-08-31 10:33:12 -04:00
Rakshith Ravi
9fc26e609c feat: allow for documentation and other attributes to fields in server fn (#2876) 2024-08-31 09:35:08 -04:00
mrvillage
4f1ee65e6c Add hash files support to 0.7 (#2894)
* Add support for JS and WASM file name hashing

* Add `<HashedStylesheet />`

* Update `<HashedStylesheet />`

* Whoops

* Fix formatting

* My IDE is just refusing to work apparently

* I hate my IDE

* Don't run the doctest

* Just remove the example, I don't know enough about doctest for this
2024-08-30 14:29:59 -07:00
Greg Johnston
ceff827a77 Merge pull request #2884 from leptos-rs/rstml-0.12
update to rstml and improve recoverability in attributes
2024-08-28 07:46:08 -04:00
Álvaro Mondéjar Rubio
a7db918775 docs: update main documentation of leptos crate (#2853) 2024-08-28 07:44:29 -04:00
Baptiste
be20ecd366 fix: implement Copy and Clone for HtmlElement without needing Rndr to be Clone/copy (#2889) 2024-08-28 07:25:51 -04:00
Greg Johnston
5790d8ad12 fix: derive various traits on Dom to make it easier to derive traits on structs that take a generic Renderer 2024-08-28 07:25:08 -04:00
luoxiaozero
7dc58e248c fix: compile attr:aria-* syntax (#2887) 2024-08-27 09:00:13 -07:00
Greg Johnston
7b03e63b23 docs: add note about curly braces 2024-08-26 21:00:45 -04:00
Greg Johnston
55fd7c6421 chore: clippy 2024-08-26 20:57:02 -04:00
Greg Johnston
ba1ea4c2bb do not error on unbraced 2024-08-26 20:56:23 -04:00
blorbb
6a4fc96835 rstml 0.12 and enforce braces in attribute values 2024-08-26 20:55:12 -04:00
Marc-Stefan Cassola
58476bb98e feat: variadic From for callbacks (#2873) 2024-08-26 20:47:11 -04:00
Oliver
88d4f14541 added attributes to source element (#2883)
Co-authored-by: Oliver Nordh <oliver.nordh@proton.me>
2024-08-26 19:27:01 -04:00
niklass-l
6bba233ba7 Add support for postcard codec in server_fn (#2879)
* Add support for postcard codec

* Add postcard feature set exclusions

* Add postcard server fn encoding example
2024-08-26 13:58:23 -07:00
Greg Johnston
1d99764740 Merge pull request #2870 from leptos-rs/dependabot/github_actions/tj-actions/changed-files-45
chore(deps): bump tj-actions/changed-files from 44 to 45
2024-08-23 17:36:21 -04:00
Álvaro Mondéjar Rubio
53cc479c14 chore: set tracing as feature for all crates (#2843) 2024-08-23 17:34:05 -04:00
Greg Johnston
d3707d9b88 Merge pull request #2865 from mkane0814/feat/add-map-and-then-for-resource
feat: add Resouce::map and Resouce::and_then
2024-08-23 17:30:57 -04:00
Marc-Stefan Cassola
1df4076fd2 updated codee version (#2874) 2024-08-23 13:50:55 -07:00
dependabot[bot]
28337bb6c9 chore(deps): bump tj-actions/changed-files from 44 to 45
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44 to 45.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v44...v45)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-21 18:22:38 +00:00
Greg Johnston
c5dea52e69 chore: restore Playwright tests for counters example (#2864) 2024-08-20 13:38:22 -04:00
Greg Johnston
d5096ff2e6 feat: allow including a view from an external file (closes #2182) (#2862) 2024-08-20 09:00:40 -04:00
Matt Kane
84734f1110 feat: add ArcResource::and_then 2024-08-20 07:41:55 -05:00
Matt Kane
7094308287 feat: add Resource::and_then 2024-08-20 00:06:56 -05:00
Matt Kane
3b88c8ccd2 feat: add Resource::map 2024-08-19 23:38:36 -05:00
112 changed files with 3519 additions and 985 deletions

View File

@@ -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"

View File

@@ -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/**

View File

@@ -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/**

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-beta2"
version = "0.7.0-beta4"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-beta2" }
throw_error = { path = "./any_error/", version = "0.2.0-beta4" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta2" }
leptos = { path = "./leptos", version = "0.7.0-beta2" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta2" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta2" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta2" }
leptos_router = { path = "./router", version = "0.7.0-beta2" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta2" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta2" }
leptos_meta = { path = "./meta", version = "0.7.0-beta2" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta2" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta4" }
leptos = { path = "./leptos", version = "0.7.0-beta4" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta4" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta4" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta4" }
leptos_router = { path = "./router", version = "0.7.0-beta4" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta4" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta4" }
leptos_meta = { path = "./meta", version = "0.7.0-beta4" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta4" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta2" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta2" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta2" }
server_fn = { path = "./server_fn", version = "0.7.0-beta2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta2" }
tachys = { path = "./tachys", version = "0.1.0-beta2" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta4" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta4" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta4" }
server_fn = { path = "./server_fn", version = "0.7.0-beta4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta4" }
tachys = { path = "./tachys", version = "0.1.0-beta4" }
[profile.release]
codegen-units = 1

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0-beta2"
version = "0.2.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -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
View File

@@ -0,0 +1,6 @@
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml

View File

@@ -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
View File

@@ -0,0 +1,4 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

83
examples/counters/e2e/package-lock.json generated Normal file
View 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"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.46.1"
},
"dependencies": {
"pnpm": "^9.7.1"
}
}

View File

@@ -0,0 +1,77 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !process.env.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,
// },
});

View 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");
});
});

View 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");
});
});

View 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");
});
});

View 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");
});
});

View 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");
});
});

View 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(),
]);
}
}

View 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");
});
});

View 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");
});
});

View 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");
});
});

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
use leptos::prelude::{signal::*, *};
use leptos::prelude::*;
const MANY_COUNTERS: usize = 1000;
@@ -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>

View File

@@ -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 \

View File

@@ -16,6 +16,7 @@ server_fn = { path = "../../server_fn", features = [
"serde-lite",
"rkyv",
"multipart",
"postcard",
] }
log = "0.4.22"
simple_logger = "5.0"

View File

@@ -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>
}
}

13
examples/static_routing/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,115 @@
[package]
name = "static_routing"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = [
"hydration",
] } #"nightly", "hydration"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
axum = { version = "0.7.5", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = [
"fs",
"rt-multi-thread",
"macros",
], optional = true }
tokio-stream = { version = "0.1", features = ["fs"], optional = true }
futures = "0.3"
wasm-bindgen = "0.2.93"
notify = { version = "6", optional = true }
http = { version = "1", optional = true }
[features]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:tokio-stream",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
"dep:notify",
"dep:http"
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "assets"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3007"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 henrik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "ssr_modes_axum"

View File

@@ -0,0 +1,11 @@
# Static Routing Example
This example shows the static routing features, which can be used to generate the HTML content for some routes before a request.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,3 @@
# My first blog post
Having a blog is *fun*.

View File

@@ -0,0 +1,3 @@
# My second blog post
Coming up with content is hard.

View File

@@ -0,0 +1,3 @@
# My third blog post
Could I just have AI write this for me instead?

View File

@@ -0,0 +1,3 @@
# My fourth post
Here is some content. It should regenerate the static page.

View File

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

View File

@@ -0,0 +1,323 @@
use std::path::Path;
use futures::{channel::mpsc, Stream};
use leptos::prelude::*;
use leptos_meta::MetaTags;
use leptos_meta::*;
use leptos_router::{
components::{FlatRoutes, Redirect, Route, Router},
hooks::use_params,
params::Params,
path,
static_routes::StaticRoute,
SsrMode,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
view! {
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
<Title text="Welcome to Leptos"/>
<Meta name="color-scheme" content="dark light"/>
<Router>
<nav>
<a href="/">"Home"</a>
</nav>
<main>
<FlatRoutes fallback>
<Route
path=path!("/")
view=HomePage
ssr=SsrMode::Static(
StaticRoute::new().regenerate(|_| watch_path(Path::new("./posts"))),
)
/>
<Route
path=path!("/about")
view=move || view! { <Redirect path="/"/> }
ssr=SsrMode::Static(StaticRoute::new())
/>
<Route
path=path!("/post/:slug/")
view=Post
ssr=SsrMode::Static(
StaticRoute::new()
.prerender_params(|| async move {
[("slug".into(), list_slugs().await.unwrap_or_default())]
.into_iter()
.collect()
})
.regenerate(|params| {
let slug = params.get("slug").unwrap();
watch_path(Path::new(&format!("./posts/{slug}.md")))
}),
)
/>
</FlatRoutes>
</main>
</Router>
}
}
#[component]
fn HomePage() -> impl IntoView {
// load the posts
let posts = Resource::new(|| (), |_| list_posts());
let posts = move || {
posts
.get()
.map(|n| n.unwrap_or_default())
.unwrap_or_default()
};
view! {
<h1>"My Great Blog"</h1>
<Suspense fallback=move || view! { <p>"Loading posts..."</p> }>
<ul>
<For each=posts key=|post| post.slug.clone() let:post>
<li>
<a href=format!("/post/{}/", post.slug)>{post.title.clone()}</a>
</li>
</For>
</ul>
</Suspense>
}
}
#[derive(Params, Clone, Debug, PartialEq, Eq)]
pub struct PostParams {
slug: Option<String>,
}
#[component]
fn Post() -> impl IntoView {
let query = use_params::<PostParams>();
let slug = move || {
query
.get()
.map(|q| q.slug.unwrap_or_default())
.map_err(|_| PostError::InvalidId)
};
let post_resource = Resource::new_blocking(slug, |slug| async move {
match slug {
Err(e) => Err(e),
Ok(slug) => get_post(slug)
.await
.map(|data| data.ok_or(PostError::PostNotFound))
.map_err(|e| PostError::ServerError(e.to_string())),
}
});
let post_view = move || {
Suspend::new(async move {
match post_resource.await {
Ok(Ok(post)) => {
Ok(view! {
<h1>{post.title.clone()}</h1>
<p>{post.content.clone()}</p>
// since we're using async rendering for this page,
// this metadata should be included in the actual HTML <head>
// when it's first served
<Title text=post.title/>
<Meta name="description" content=post.content/>
})
}
Ok(Err(e)) | Err(e) => {
Err(PostError::ServerError(e.to_string()))
}
}
})
};
view! {
<em>"The world's best content."</em>
<Suspense fallback=move || view! { <p>"Loading post..."</p> }>
<ErrorBoundary fallback=|errors| {
#[cfg(feature = "ssr")]
expect_context::<leptos_axum::ResponseOptions>()
.set_status(http::StatusCode::NOT_FOUND);
view! {
<div class="error">
<h1>"Something went wrong."</h1>
<ul>
{move || {
errors
.get()
.into_iter()
.map(|(_, error)| view! { <li>{error.to_string()}</li> })
.collect::<Vec<_>>()
}}
</ul>
</div>
}
}>{post_view}</ErrorBoundary>
</Suspense>
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PostError {
#[error("Invalid post ID.")]
InvalidId,
#[error("Post not found.")]
PostNotFound,
#[error("Server error: {0}.")]
ServerError(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Post {
slug: String,
title: String,
content: String,
}
#[server]
pub async fn list_slugs() -> Result<Vec<String>, ServerFnError> {
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
use tokio_stream::StreamExt;
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
Ok(files
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if !path.is_file() {
return None;
}
let extension = path.extension()?;
if extension != "md" {
return None;
}
let slug = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.replace(".md", "");
Some(slug)
})
.collect()
.await)
}
#[server]
pub async fn list_posts() -> Result<Vec<Post>, ServerFnError> {
println!("calling list_posts");
use futures::TryStreamExt;
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
let files = ReadDirStream::new(fs::read_dir("./posts").await?);
files
.try_filter_map(|entry| async move {
let path = entry.path();
if !path.is_file() {
return Ok(None);
}
let Some(extension) = path.extension() else {
return Ok(None);
};
if extension != "md" {
return Ok(None);
}
let slug = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.replace(".md", "");
let content = fs::read_to_string(path).await?;
// world's worst Markdown frontmatter parser
let title = content.lines().next().unwrap().replace("# ", "");
Ok(Some(Post {
slug,
title,
content,
}))
})
.try_collect()
.await
.map_err(ServerFnError::from)
}
#[server]
pub async fn get_post(slug: String) -> Result<Option<Post>, ServerFnError> {
println!("reading ./posts/{slug}.md");
let content =
tokio::fs::read_to_string(&format!("./posts/{slug}.md")).await?;
// world's worst Markdown frontmatter parser
let title = content.lines().next().unwrap().replace("# ", "");
Ok(Some(Post {
slug,
title,
content,
}))
}
#[allow(unused)] // path is not used in non-SSR
fn watch_path(path: &Path) -> impl Stream<Item = ()> {
#[allow(unused)]
let (mut tx, rx) = mpsc::channel(0);
#[cfg(feature = "ssr")]
{
use notify::RecursiveMode;
use notify::Watcher;
let mut watcher =
notify::recommended_watcher(move |res: Result<_, _>| {
if res.is_ok() {
// if this fails, it's because the buffer is full
// this means we've already notified before it's regenerated,
// so this page will be queued for regeneration already
_ = tx.try_send(());
}
})
.expect("could not create watcher");
// Add a path to be watched. All files and directories at that path and
// below will be monitored for changes.
watcher
.watch(path, RecursiveMode::NonRecursive)
.expect("could not watch path");
// we want this to run as long as the server is alive
std::mem::forget(watcher);
}
rx
}

View File

@@ -0,0 +1,9 @@
pub mod app;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

View File

@@ -0,0 +1,42 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list_with_ssg, LeptosRoutes};
use static_routing::app::*;
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let (routes, static_routes) = generate_route_list_with_ssg({
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
});
static_routes.generate(&leptos_options).await;
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -0,0 +1,3 @@
body {
font-family: sans-serif;
}

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0-beta2"
version = "0.2.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -10,6 +10,7 @@ edition.workspace = true
[dependencies]
actix-http = "3.8"
actix-files = "0.6"
actix-web = "4.8"
futures = "0.3.30"
any_spawner = { workspace = true, features = ["tokio"] }
@@ -22,12 +23,18 @@ 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"
dashmap = "6"
once_cell = "1"
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -6,30 +6,38 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use actix_files::NamedFile;
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::{Payload, ServiceConfig},
test,
web::{Data, Payload, ServiceConfig},
*,
};
use dashmap::DashMap;
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::expect_context,
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView, *,
IntoView,
};
use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
use leptos_router::{
components::provide_server_redirect, location::RequestUrl, PathSegment,
RouteList, RouteListing, SsrMode, StaticDataMap, StaticMode, *,
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, ResolvedStaticPath},
Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
@@ -37,7 +45,9 @@ use server_fn::{
};
use std::{
fmt::{Debug, Display},
future::Future,
ops::{Deref, DerefMut},
path::Path,
sync::Arc,
};
@@ -207,7 +217,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 +252,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 +299,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 +326,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 +461,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 +531,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 +596,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 +622,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 +659,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 +693,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 +726,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,
@@ -694,17 +738,32 @@ pub fn render_app_async_with_context<IV>(
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = app.to_html_stream_in_order().collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
handle_response(method, additional_context, app_fn, async_stream_builder)
}
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
})
}
#[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,
@@ -785,7 +844,7 @@ where
/// create routes in Actix's App 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 generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> Vec<ActixRouteListing>
where
IV: IntoView + 'static,
@@ -797,8 +856,8 @@ where
/// create routes in Actix's App 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 generated Actix compatible paths.
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<ActixRouteListing>, StaticDataMap)
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -810,7 +869,7 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix 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 Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<ActixRouteListing>
where
@@ -824,9 +883,9 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix 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 Actix path format
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<ActixRouteListing>, StaticDataMap)
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -875,7 +934,7 @@ pub struct ActixRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: Vec<RegenerationFn>,
}
impl From<RouteListing> for ActixRouteListing {
@@ -888,12 +947,12 @@ impl From<RouteListing> for ActixRouteListing {
};
let mode = value.mode();
let methods = value.methods().collect();
let static_mode = value.into_static_parts();
let regenerate = value.regenerate().into();
Self {
path,
mode,
mode: mode.clone(),
methods,
static_mode,
regenerate,
}
}
}
@@ -904,13 +963,13 @@ impl ActixRouteListing {
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl Into<Vec<RegenerationFn>>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into(),
}
}
@@ -921,19 +980,13 @@ impl ActixRouteListing {
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
self.mode.clone()
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
}
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -942,10 +995,10 @@ impl ActixRouteListing {
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format.
/// Additional context will be provided to the app Element.
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<ActixRouteListing>, StaticDataMap)
additional_context: impl Fn() + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
@@ -964,6 +1017,12 @@ where
})
.unwrap_or_default();
let generator = StaticRouteGenerator::new(
&routes,
app_fn.clone(),
additional_context.clone(),
);
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_inner()
@@ -977,7 +1036,7 @@ where
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
None,
vec![],
)]
} else {
// Routes to exclude from auto generation
@@ -987,192 +1046,251 @@ where
}
routes
},
StaticDataMap::new(), // TODO
//static_data_map,
generator,
)
}
/// Allows generating any prerendered routes.
#[allow(clippy::type_complexity)]
pub struct StaticRouteGenerator(
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
);
impl StaticRouteGenerator {
fn render_route<IV: IntoView + 'static>(
path: String,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> impl Future<Output = (Owner, String)> {
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let add_context = additional_context.clone();
move || {
let mock_req = test::TestRequest::with_uri(&path)
.insert_header(("Accept", "text/html"))
.to_http_request();
let res_options = ResponseOptions::default();
provide_contexts(
Request::new(&mock_req),
&meta_context,
&res_options,
);
add_context();
}
};
let (owner, stream) = leptos_integration_utils::build_response(
app_fn.clone(),
additional_context,
async_stream_builder,
);
let sc = owner.shared_context().unwrap();
async move {
let stream = stream.await;
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let html = meta_output
.inject_meta_context(stream)
.await
.collect::<String>()
.await;
(owner, html)
}
}
/// Creates a new static route generator from the given list of route definitions.
pub fn new<IV>(
routes: &RouteList,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
Self({
let routes = routes.clone();
Box::new(move |options| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
Box::pin(routes.generate_static_files(
move |path: &ResolvedStaticPath| {
Self::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
))
})
})
}
/// Generates the routes.
pub async fn generate(self, options: &LeptosOptions) {
(self.0)(options).await
}
}
static STATIC_HEADERS: Lazy<DashMap<String, ResponseOptions>> =
Lazy::new(DashMap::new);
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
let status = resp.0.read().status;
if let Some(status) = status {
return status == StatusCode::NOT_FOUND;
}
false
}
fn static_path(options: &LeptosOptions, path: &str) -> String {
use leptos_integration_utils::static_file_path;
// If the path ends with a trailing slash, we generate the path
// as a directory with a index.html file inside.
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{}index", path))
} else {
static_file_path(options, path)
}
}
async fn write_static_route(
options: &LeptosOptions,
response_options: Option<ResponseOptions>,
path: &str,
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
}
let path = static_path(options, path);
let path = Path::new(&path);
if let Some(path) = path.parent() {
tokio::fs::create_dir_all(path).await?;
}
tokio::fs::write(path, &html).await?;
Ok(())
}
fn handle_static_route<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
regenerate: Vec<RegenerationFn>,
) -> Route
where
IV: IntoView + 'static,
{
let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
Box::pin({
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let regenerate = regenerate.clone();
async move {
let options = data.into_inner();
let orig_path = req.uri().path();
let path = static_path(&options, orig_path);
let path = Path::new(&path);
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
let (response_options, html) = if !exists {
let path = ResolvedStaticPath::new(orig_path);
let (owner, html) = path
.build(
move |path: &ResolvedStaticPath| {
StaticRouteGenerator::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
regenerate,
)
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers =
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
(headers, None)
};
// if html is Some(_), it means that `was_error_response` is true and we're not
// actually going to cache this route, just return it as HTML
//
// this if for thing like 404s, where we do not want to cache an endless series of
// typos (or malicious requests)
let mut res = ActixResponse(match html {
Some(html) => {
HttpResponse::Ok().content_type("text/html").body(html)
}
None => match NamedFile::open(path) {
Ok(res) => res.into_response(&req),
Err(err) => HttpResponse::InternalServerError()
.body(err.to_string()),
},
});
if let Some(options) = response_options {
res.extend_response(&options);
}
res.0
}
})
};
web::get().to(handler)
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
// TODO static response
/*
fn handle_static_response<'a, IV>(
path: &'a str,
options: &'a LeptosOptions,
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
additional_context: &'a (impl Fn() + 'static + Clone + Send),
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = HttpResponse::new(match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
});
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
res.set_body(body)
}
StaticResponse::RenderDynamic => {
handle_static_response(
path,
options,
app_fn,
additional_context,
render_dynamic(
path,
options,
app_fn.clone(),
additional_context.clone(),
)
.await,
)
.await
}
StaticResponse::RenderNotFound => {
handle_static_response(
path,
options,
app_fn,
additional_context,
not_found_page(
tokio::fs::read_to_string(not_found_path(options))
.await,
),
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
method: Method,
mode: StaticMode,
) -> Route
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
StaticMode::Upfront => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
}
}
*/
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
@@ -1205,7 +1323,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 +1338,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>,
@@ -1247,19 +1371,15 @@ where
provide_context(method);
additional_context();
};
router = if let Some(static_mode) = listing.static_mode() {
_ = static_mode;
todo!() /*
router.route(
path,
static_route(
app_fn.clone(),
additional_context_and_method.clone(),
method,
static_mode,
),
)
*/
router = if matches!(listing.mode(), SsrMode::Static(_)) {
router.route(
path,
handle_static_route(
additional_context_and_method.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router.route(
path,
@@ -1291,6 +1411,7 @@ where
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
@@ -1304,7 +1425,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 +1440,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>,
@@ -1341,7 +1468,17 @@ impl LeptosRoutes for &mut ServiceConfig {
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
if matches!(listing.mode(), SsrMode::Static(_)) {
router = router.route(
path,
handle_static_route(
additional_context.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
@@ -1371,8 +1508,10 @@ impl LeptosRoutes for &mut ServiceConfig {
app_fn.clone(),
method,
),
_ => unreachable!()
},
);
}
}
}

View File

@@ -14,6 +14,7 @@ hydration_context = { workspace = true }
axum = { version = "0.7.5", default-features = false, features = [
"matched-path",
] }
dashmap = "6"
futures = "0.3.30"
http = "1.1"
http-body-util = "0.1.2"
@@ -23,12 +24,13 @@ leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
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 +40,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"]

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -5,6 +5,7 @@ use leptos::{
reactive_graph::owner::{Owner, Sandboxed},
IntoView,
};
use leptos_config::LeptosOptions;
use leptos_meta::ServerMetaContextOutput;
use std::{future::Future, pin::Pin, sync::Arc};
@@ -132,3 +133,13 @@ where
}));
(owner, stream)
}
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
let trimmed_path = path.trim_start_matches('/');
let path = if trimmed_path.is_empty() {
"index"
} else {
trimmed_path
};
format!("{}/{}.html", options.site_root, path)
}

View File

@@ -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]

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -39,12 +39,36 @@ pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
) -> impl IntoView {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_output_name.push_str("_bg");
let mut js_file_name = options.output_name.to_string();
let mut wasm_file_name = options.output_name.to_string();
if options.hash_files {
let hash_path = std::env::current_exe()
.map(|path| {
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
})
.unwrap_or_default()
.join(&options.hash_file);
if hash_path.exists() {
let hashes = std::fs::read_to_string(&hash_path)
.expect("failed to read hash file");
for line in hashes.lines() {
let line = line.trim();
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "js" {
js_file_name.push_str(&format!(".{}", hash));
} else if file == "wasm" {
wasm_file_name.push_str(&format!(".{}", hash));
}
}
}
}
}
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_file_name.push_str("_bg");
}
let pkg_path = &options.site_pkg_dir;
#[cfg(feature = "nonce")]
let nonce = crate::nonce::use_nonce();
#[cfg(not(feature = "nonce"))]
@@ -59,16 +83,16 @@ pub fn HydrationScripts(
};
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="modulepreload" href=format!("/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("/{pkg_path}/{wasm_output_name}.wasm")
href=format!("/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
{format!("{script}({pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
</script>
}
}

View File

@@ -4,13 +4,13 @@
//!
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
//! - single-page apps (SPAs) rendered entirely in the browser, using client-side routing and loading
//! or mutating data via async requests to the server
//! or mutating data via async requests to the server.
//! - multi-page apps (MPAs) rendered on the server, managing navigation, data, and mutations via
//! web-standard `<a>` and `<form>` tags
//! web-standard `<a>` and `<form>` tags.
//! - progressively-enhanced single-page apps that are rendered on the server and then hydrated on the client,
//! enhancing your `<a>` and `<form>` navigations and mutations seamlessly when WASM is available.
//!
//! And you can do all three of these **using the same Leptos code.**
//! And you can do all three of these **using the same Leptos code**.
//!
//! Take a look at the [Leptos Book](https://leptos-rs.github.io/leptos/) for a walkthrough of the framework.
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
@@ -20,76 +20,76 @@
//!
//! If you want to see what Leptos is capable of, check out
//! the [examples](https://github.com/leptos-rs/leptos/tree/main/examples):
//! - [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counter_without_macros`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros)
//! adapts the counter example to use the builder pattern for the UI and avoids other macros, instead showing
//! the code that Leptos generates.
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
//! `Result` types to handle errors.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`fetch`](https://github.com/leptos-rs/leptos/tree/main/examples/fetch) introduces
//! [Resource]s, which allow you to integrate arbitrary `async` code like an
//!
//! - **[`counter`]** is the classic counter example, showing the basics of client-side rendering and reactive DOM updates.
//! - **[`counter_without_macros`]** adapts the counter example to use the builder pattern for the UI and avoids other macros,
//! instead showing the code that Leptos generates.
//! - **[`counters`]** introduces parent-child communication via contexts, and the [`<For/>`](leptos::prelude::For) component
//! for efficient keyed list updates.
//! - **[`error_boundary`]** shows how to use [`Result`] types to handle errors.
//! - **[`parent_child`]** shows four different ways a parent component can communicate with a child, including passing a closure,
//! context, and more.
//! - **[`fetch`]** introduces [`Resource`](leptos::prelude::Resource)s, which allow you to integrate arbitrary `async` code like an
//! HTTP request within your reactive code.
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading.
//! - [`slots`](https://github.com/leptos-rs/leptos/tree/main/examples/slots) shows how to use slots on components.
//! - [`spread`](https://github.com/leptos-rs/leptos/tree/main/examples/spread) shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
//! and server-sent events (SSE).
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
//! - **[`router`]** shows how to use Leptoss nested router to enable client-side navigation and route-specific, reactive data loading.
//! - **[`slots`]** shows how to use slots on components.
//! - **[`spread`]** shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - **[`counter_isomorphic`]** shows different methods of interaction with a stateful server, including server functions,
//! server actions, forms, and server-sent events (SSE).
//! - **[`todomvc`]** shows the basics of building an isomorphic web app. Both the server and the client import the same app code.
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
//! You might also want to
//! see how we use [`create_effect`] to [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L164)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L291)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/6d7c36655c9e7dcc3a3ad33d2b846a3f00e4ae74/examples/todomvc/src/lib.rs#L254).
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
//! show how to build a full-stack app using server functions and database connections.
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) shows how to integrate
//! TailwindCSS with `trunk` for CSR.
//! You might also want to see how we use [`Effect::new`](leptos::prelude::Effect::new) to
//! [serialize JSON to `localStorage`](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L191-L209)
//! and [reactively call DOM methods](https://github.com/leptos-rs/leptos/blob/16f084a71268ac325fbc4a5e50c260df185eadb6/examples/todomvc/src/lib.rs#L292-L296)
//! on [references to elements](https://github.com/leptos-rs/leptos/blob/20af4928b2fffe017408d3f4e7330db22cf68277/examples/todomvc/src/lib.rs#L228).
//! - **[`hackernews`]** and **[`hackernews_axum`]** integrate calls to a real external REST API, routing, server-side rendering and
//! hydration to create a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - **[`todo_app_sqlite`]** and **[`todo_app_sqlite_axum`]** show how to build a full-stack app using server functions and
//! database connections.
//! - **[`tailwind`]** shows how to integrate TailwindCSS with `trunk` for CSR.
//!
//! [`counter`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter
//! [`counter_without_macros`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros
//! [`counters`]: https://github.com/leptos-rs/leptos/tree/main/examples/counters
//! [`error_boundary`]: https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary
//! [`parent_child`]: https://github.com/leptos-rs/leptos/tree/main/examples/parent_child
//! [`fetch`]: https://github.com/leptos-rs/leptos/tree/main/examples/fetch
//! [`router`]: https://github.com/leptos-rs/leptos/tree/main/examples/router
//! [`slots`]: https://github.com/leptos-rs/leptos/tree/main/examples/slots
//! [`spread`]: https://github.com/leptos-rs/leptos/tree/main/examples/spread
//! [`counter_isomorphic`]: https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic
//! [`todomvc`]: https://github.com/leptos-rs/leptos/tree/main/examples/todomvc
//! [`hackernews`]: https://github.com/leptos-rs/leptos/tree/main/examples/hackernews
//! [`hackernews_axum`]: https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum
//! [`todo_app_sqlite`]: https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite
//! [`todo_app_sqlite_axum`]: https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum
//! [`tailwind`]: https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr
//!
//! Details on how to run each example can be found in its README.
//!
//! # Quick Links
//!
//! Here are links to the most important sections of the docs:
//! - **Reactivity**: the [`leptos_reactive`] overview, and more details in
//! - signals: [`create_signal`], [`ReadSignal`], and [`WriteSignal`] (and [`create_rw_signal`] and [`RwSignal`])
//! - computations: [`create_memo`] and [`Memo`]
//! - `async` interop: [`create_resource`] and [`Resource`] for loading data using `async` functions,
//! and [`create_action`] and [`Action`] to mutate data or imperatively call `async` functions.
//! - reactions: [`create_effect`]
//! - **Templating/Views**: the [`view`] macro
//! - **Reactivity**: the [`reactive_graph`] overview, and more details in
//! + signals: [`signal`](leptos::prelude::signal), [`ReadSignal`](leptos::prelude::ReadSignal),
//! [`WriteSignal`](leptos::prelude::WriteSignal) and [`RwSignal`](leptos::prelude::RwSignal).
//! + computations: [`Memo`](leptos::prelude::Memo).
//! + `async` interop: [`Resource`](leptos::prelude::Resource) for loading data using `async` functions
//! and [`Action`](leptos::prelude::Action) to mutate data or imperatively call `async` functions.
//! + reactions: [`Effect`](leptos::prelude::Effect) and [`RenderEffect`](leptos::prelude::RenderEffect).
//! - **Templating/Views**: the [`view`] macro and [`IntoView`](leptos::IntoView) trait.
//! - **Routing**: the [`leptos_router`](https://docs.rs/leptos_router/latest/leptos_router/) crate
//! - **Server Functions**: the [`server`](crate::leptos_server) macro, [`create_action`], and [`create_server_action`]
//! - **Server Functions**: the [`server`](macro@leptos::prelude::server) macro and [`ServerAction`](leptos::prelude::ServerAction).
//!
//! # Feature Flags
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `serde` (*Default*) In SSR/hydrate mode, uses [`serde`](https://docs.rs/serde/latest/serde/) to serialize resources and send them
//!
//! - **`nightly`**: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
//! - **`csr`** Client-side rendering: Generate DOM nodes in the browser.
//! - **`ssr`** Server-side rendering: Generate an HTML string (typically on the server).
//! - **`hydrate`** Hydration: use this to add interactivity to an SSRed Leptos app.
//! - **`rkyv`** In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `serde-lite` In SSR/hydrate mode, uses [`serde-lite`](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
//! from the server to the client.
//! - `rkyv` In SSR/hydrate mode, uses [`rkyv`](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `miniserde` In SSR/hydrate mode, uses [`miniserde`](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
//! from the server to the client.
//! - `tracing` Adds additional support for [`tracing`](https://docs.rs/tracing/latest/tracing/) to components.
//! - `default-tls` Use default native TLS support. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `rustls` Use `rustls`. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `template_macro` Enables the [`template!`](leptos_macro::template) macro, which offers faster DOM node creation for some use cases in `csr`.
//! - **`tracing`** Adds support for [`tracing`](https://docs.rs/tracing/latest/tracing/).
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in. You should only enable one of these per build target,
@@ -103,7 +103,7 @@
//! #[component]
//! pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
//! // create a reactive signal with the initial value
//! let (value, set_value) = signal( initial_value);
//! let (value, set_value) = signal(initial_value);
//!
//! // create event handlers for our buttons
//! // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -112,7 +112,6 @@
//! let increment = move |_| *set_value.write() += 1;
//!
//! view! {
//!
//! <div>
//! <button on:click=clear>"Clear"</button>
//! <button on:click=decrement>"-1"</button>
@@ -124,7 +123,8 @@
//! ```
//!
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
//! ```
//!
//! ```rust
//! use leptos::{mount::mount_to_body, prelude::*};
//!
//! #[component]
@@ -169,7 +169,7 @@ pub mod prelude {
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, owner::*, signal::*,
wrappers::read::*, *,
wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
@@ -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::*;

View File

@@ -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

View File

@@ -264,7 +264,6 @@ where
{
buf.next_id();
let suspense_context = use_context::<SuspenseContext>().unwrap();
let owner = Owner::current().unwrap();
// we need to wait for one of two things: either
@@ -277,6 +276,16 @@ where
futures::channel::oneshot::channel::<()>();
let mut tasks_tx = Some(tasks_tx);
// now, create listener for local resources
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
// check the set of tasks to see if it is empty, now or later
let eff = reactive_graph::effect::RenderEffect::new_isomorphic({
move |_| {
tasks.track();
@@ -290,14 +299,6 @@ where
}
});
// now, create listener for local resources
let (local_tx, mut local_rx) =
futures::channel::oneshot::channel::<()>();
provide_context(LocalResourceNotifier::from(local_tx));
// walk over the tree of children once to make sure that all resource loads are registered
self.children.dry_resolve();
let mut fut = Box::pin(ScopedFuture::new(ErrorHookFuture::new(
async move {
// race the local resource notifier against the set of tasks

View File

@@ -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"]

View File

@@ -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]

View File

@@ -0,0 +1,3 @@
#[cfg(feature = "trace-component-props")]
#[doc(hidden)]
pub mod tracing_property;

View 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");
}

View File

@@ -20,7 +20,7 @@ syn = { version = "2.0", features = [
"printing",
] }
quote = "1.0"
rstml = "0.11.2"
rstml = "0.12.0"
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
parking_lot = "0.12.3"
walkdir = "2.5"

View File

@@ -1,4 +1,4 @@
use rstml::node::{NodeElement, NodeName};
use rstml::node::{CustomNode, NodeElement, NodeName};
/// Converts `syn::Block` to simple expression
///
@@ -65,6 +65,6 @@ pub fn is_component_tag_name(name: &NodeName) -> bool {
}
#[must_use]
pub fn is_component_node(node: &NodeElement) -> bool {
pub fn is_component_node(node: &NodeElement<impl CustomNode>) -> bool {
is_component_tag_name(node.name())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-beta2"
version = "0.7.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -22,12 +22,12 @@ proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
rstml = "0.11.2"
rstml = "0.12.0"
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"]

View File

@@ -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();

View File

@@ -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;
@@ -73,6 +74,9 @@ mod slot;
/// Attributes can take a wide variety of primitive types that can be converted to strings. They can also
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
///
/// Note that in some cases, rust-analyzer support may be better if attribute values are surrounded with braces (`{}`).
/// Unlike in JSX, attribute values are not required to be in braces, but braces can be used and may improve this LSP support.
///
/// ```rust,ignore
/// # use leptos::prelude::*;
///
@@ -260,10 +264,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();
@@ -308,10 +309,17 @@ pub fn view(tokens: TokenStream) -> TokenStream {
global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()),
);
// The allow lint needs to be put here instead of at the expansion of
// view::attribute_value(). Adding this next to the expanded expression
// seems to break rust-analyzer, but it works when the allow is put here.
quote! {
{
#(#errors;)*
#nodes_output
#[allow(unused_braces)]
{
#(#errors;)*
#nodes_output
}
}
}
.into()
@@ -331,6 +339,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

View File

@@ -3,13 +3,14 @@ use crate::view::attribute_absolute;
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{
KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement, NodeName,
CustomNode, KeyedAttributeValue, NodeAttribute, NodeBlock, NodeElement,
NodeName,
};
use std::collections::HashMap;
use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
pub(crate) fn component_to_tokens(
node: &NodeElement,
node: &NodeElement<impl CustomNode>,
global_class: Option<&TokenTree>,
) -> TokenStream {
let name = node.name();

View File

@@ -10,8 +10,8 @@ use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use proc_macro_error::abort;
use quote::{quote, quote_spanned, ToTokens};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
NodeNameFragment,
CustomNode, KVAttributeValue, KeyedAttribute, Node, NodeAttribute,
NodeBlock, NodeElement, NodeName, NodeNameFragment,
};
use std::collections::{HashMap, HashSet};
use syn::{
@@ -89,7 +89,7 @@ pub fn render_view(
}
fn element_children_to_tokens(
nodes: &[Node],
nodes: &[Node<impl CustomNode>],
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -117,7 +117,7 @@ fn element_children_to_tokens(
}
fn fragment_to_tokens(
nodes: &[Node],
nodes: &[Node<impl CustomNode>],
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -142,7 +142,7 @@ fn fragment_to_tokens(
}
fn children_to_tokens(
nodes: &[Node],
nodes: &[Node<impl CustomNode>],
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -186,7 +186,7 @@ fn children_to_tokens(
}
fn node_to_tokens(
node: &Node,
node: &Node<impl CustomNode>,
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -219,6 +219,7 @@ fn node_to_tokens(
global_class,
view_marker,
),
Node::Custom(node) => Some(node.to_token_stream()),
}
}
@@ -236,7 +237,7 @@ fn text_to_tokens(text: &LitStr) -> TokenStream {
}
pub(crate) fn element_to_tokens(
node: &NodeElement,
node: &NodeElement<impl CustomNode>,
mut parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -411,7 +412,7 @@ pub(crate) fn element_to_tokens(
}
}
fn is_spread_marker(node: &NodeElement) -> bool {
fn is_spread_marker(node: &NodeElement<impl CustomNode>) -> bool {
match node.name() {
NodeName::Block(block) => matches!(
block.stmts.first(),
@@ -547,10 +548,12 @@ pub(crate) fn attribute_absolute(
node: &KeyedAttribute,
after_spread: bool,
) -> Option<TokenStream> {
let contains_dash = node.key.to_string().contains('-');
let key = node.key.to_string();
let contains_dash = key.contains('-');
let attr_aira = key.starts_with("attr:aria-");
// anything that follows the x:y pattern
match &node.key {
NodeName::Punctuated(parts) if !contains_dash => {
NodeName::Punctuated(parts) if !contains_dash || attr_aira => {
if parts.len() >= 2 {
let id = &parts[0];
match id {
@@ -566,6 +569,14 @@ pub(crate) fn attribute_absolute(
Some(
quote! { ::leptos::tachys::html::#key::#key(#value) },
)
} else if key_name == "aria" {
let mut parts_iter = parts.iter();
parts_iter.next();
let fn_name = parts_iter.map(|p| p.to_string()).collect::<Vec<String>>().join("_");
let key = Ident::new(&fn_name, key.span());
Some(
quote! { ::leptos::tachys::html::attribute::#key(#value) },
)
} else {
Some(
quote! { ::leptos::tachys::html::attribute::#key(#value) },
@@ -763,7 +774,7 @@ fn is_custom_element(tag: &str) -> bool {
tag.contains('-')
}
fn is_self_closing(node: &NodeElement) -> bool {
fn is_self_closing(node: &NodeElement<impl CustomNode>) -> bool {
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
[
@@ -911,20 +922,31 @@ fn attribute_name(name: &NodeName) -> TokenStream {
}
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
match attr.value() {
Some(value) => {
if let Expr::Lit(lit) = value {
if cfg!(feature = "nightly") {
if let Lit::Str(str) = &lit.lit {
return quote! {
::leptos::tachys::view::static_types::Static::<#str>
};
match attr.possible_value.to_value() {
None => quote! { true },
Some(value) => match &value.value {
KVAttributeValue::Expr(expr) => {
if let Expr::Lit(lit) = expr {
if cfg!(feature = "nightly") {
if let Lit::Str(str) = &lit.lit {
return quote! {
::leptos::tachys::view::static_types::Static::<#str>
};
}
}
}
quote! {
{#expr}
}
}
quote! { #value }
}
None => quote! { true },
// any value in braces: expand as-is to give proper r-a support
KVAttributeValue::InvalidBraced(block) => {
quote! {
#block
}
}
},
}
}

View File

@@ -2,12 +2,12 @@ use super::{convert_to_snake_case, ident_from_tag_name};
use crate::view::{fragment_to_tokens, TagType};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{KeyedAttribute, NodeAttribute, NodeElement};
use rstml::node::{CustomNode, KeyedAttribute, NodeAttribute, NodeElement};
use std::collections::HashMap;
use syn::spanned::Spanned;
pub(crate) fn slot_to_tokens(
node: &NodeElement,
node: &NodeElement<impl CustomNode>,
slot: &KeyedAttribute,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
@@ -213,7 +213,9 @@ pub(crate) fn is_slot(node: &KeyedAttribute) -> bool {
key == "slot" || key.starts_with("slot:")
}
pub(crate) fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> {
pub(crate) fn get_slot(
node: &NodeElement<impl CustomNode>,
) -> Option<&KeyedAttribute> {
node.attributes().iter().find_map(|node| {
if let NodeAttribute::Attribute(node) = node {
if is_slot(node) {

View File

@@ -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"]

View File

@@ -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

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0-beta3"
version = "0.7.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -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"]

View File

@@ -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 }

View File

@@ -1,7 +1,7 @@
use crate::register;
use leptos::{
attr::global::GlobalAttributes, component, tachys::html::element::link,
IntoView,
attr::global::GlobalAttributes, component, prelude::LeptosOptions,
tachys::html::element::link, IntoView,
};
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
@@ -34,3 +34,46 @@ pub fn Stylesheet(
// TODO additional attributes
register(link().id(id).rel("stylesheet").href(href))
}
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
#[component]
pub fn HashedStylesheet(
/// Leptos options
options: LeptosOptions,
/// An ID for the stylesheet.
#[prop(optional, into)]
id: Option<String>,
) -> impl IntoView {
let mut css_file_name = options.output_name.to_string();
if options.hash_files {
let hash_path = std::env::current_exe()
.map(|path| {
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
})
.unwrap_or_default()
.join(&options.hash_file);
if hash_path.exists() {
let hashes = std::fs::read_to_string(&hash_path)
.expect("failed to read hash file");
for line in hashes.lines() {
let line = line.trim();
if !line.is_empty() {
if let Some((file, hash)) = line.split_once(':') {
if file == "css" {
css_file_name.push_str(&format!(".{}", hash));
}
}
}
}
}
}
css_file_name.push_str(".css");
let pkg_path = &options.site_pkg_dir;
// TODO additional attributes
register(
link()
.id(id)
.rel("stylesheet")
.href(format!("/{pkg_path}/{css_file_name}")),
)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "next_tuple"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -44,3 +44,6 @@ sandboxed-arenas = []
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -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,

View File

@@ -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,

View File

@@ -1,6 +1,9 @@
use crate::owner::Owner;
use or_poisoned::OrPoisoned;
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
collections::VecDeque,
};
impl Owner {
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
@@ -60,6 +63,35 @@ impl Owner {
None
}
}
/// Searches for items stored in context in either direction, either among parents or among
/// descendants.
pub fn use_context_bidirectional<T: Clone + 'static>(&self) -> Option<T> {
self.use_context()
.unwrap_or_else(|| self.find_context_in_children())
}
fn find_context_in_children<T: Clone + 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
let inner = self.inner.read().or_poisoned();
let mut to_search = VecDeque::new();
to_search.extend(inner.children.clone());
drop(inner);
while let Some(next) = to_search.pop_front() {
if let Some(child) = next.upgrade() {
let child = child.read().or_poisoned();
let contexts = &child.contexts;
if let Some(context) = contexts.get(&ty) {
return context.downcast_ref::<T>().cloned();
}
to_search.extend(child.children.clone());
}
}
None
}
}
/// Provides a context value of type `T` to the current reactive [`Owner`]

View File

@@ -580,7 +580,7 @@ impl<T, S> Dispose for StoredValue<T, S> {
#[inline(always)]
#[track_caller]
#[deprecated(
since = "0.7.0-beta2",
since = "0.7.0-beta4",
note = "This function is being removed to conform to Rust idioms. Please \
use `StoredValue::new()` or `StoredValue::new_local()` instead."
)]

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
rust-version.workspace = true
edition.workspace = true

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.1.0-beta2"
version = "0.1.0-beta4"
rust-version.workspace = true
edition.workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.0-beta2"
version = "0.7.0-beta4"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -28,6 +28,7 @@ send_wrapper = "0.6.0"
thiserror = "1.0"
percent-encoding = { version = "2.3", optional = true }
gloo-net = "0.6.0"
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3.70"
@@ -64,3 +65,6 @@ nightly = []
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]

View File

@@ -450,6 +450,12 @@ pub fn Redirect<P>(
"Calling <Redirect/> without a ServerRedirectFunction \
provided, in SSR mode."
);
#[cfg(not(feature = "tracing"))]
eprintln!(
"Calling <Redirect/> without a ServerRedirectFunction \
provided, in SSR mode."
);
return;
}
let navigate = use_navigate();

View File

@@ -2,8 +2,8 @@ use crate::{
location::{LocationProvider, Url},
matching::Routes,
params::ParamsMap,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
RouteList, RouteListing, RouteMatchId,
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
@@ -511,10 +511,8 @@ where
RouteListing::new(
path,
data.ssr_mode,
// TODO methods
[Method::Get],
// TODO static data
None,
data.methods,
data.regenerate,
)
})
.collect::<Vec<_>>();

View File

@@ -1,9 +1,17 @@
use crate::{
matching::PathSegment, Method, SsrMode, StaticDataMap, StaticMode,
matching::PathSegment,
static_routes::{
RegenerationFn, ResolvedStaticPath, StaticPath, StaticRoute,
},
Method, SsrMode,
};
use futures::future::join_all;
use reactive_graph::owner::Owner;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
future::Future,
mem,
};
use tachys::{renderer::Renderer, view::RenderHtml};
@@ -13,7 +21,7 @@ pub struct RouteListing {
path: Vec<PathSegment>,
mode: SsrMode,
methods: HashSet<Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: Vec<RegenerationFn>,
}
impl RouteListing {
@@ -22,19 +30,19 @@ impl RouteListing {
path: impl IntoIterator<Item = PathSegment>,
mode: SsrMode,
methods: impl IntoIterator<Item = Method>,
static_mode: Option<(StaticMode, StaticDataMap)>,
regenerate: impl IntoIterator<Item = RegenerationFn>,
) -> Self {
Self {
path: path.into_iter().collect(),
mode,
methods: methods.into_iter().collect(),
static_mode,
regenerate: regenerate.into_iter().collect(),
}
}
/// Create a route listing from a path, with the other fields set to default values.
pub fn from_path(path: impl IntoIterator<Item = PathSegment>) -> Self {
Self::new(path, SsrMode::Async, [], None)
Self::new(path, SsrMode::Async, [], [])
}
/// The path this route handles.
@@ -43,8 +51,8 @@ impl RouteListing {
}
/// The rendering mode for this path.
pub fn mode(&self) -> SsrMode {
self.mode
pub fn mode(&self) -> &SsrMode {
&self.mode
}
/// The HTTP request methods this path can handle.
@@ -52,56 +60,95 @@ impl RouteListing {
self.methods.iter().copied()
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_mode(&self) -> Option<StaticMode> {
self.static_mode.as_ref().map(|n| n.0)
/// The set of regeneration functions that should be applied to this route, if it is statically
/// generated (either up front or incrementally).
pub fn regenerate(&self) -> &[RegenerationFn] {
&self.regenerate
}
/// Whether this route is statically rendered.
#[inline(always)]
pub fn static_data_map(&self) -> Option<&StaticDataMap> {
self.static_mode.as_ref().map(|n| &n.1)
pub fn static_route(&self) -> Option<&StaticRoute> {
match self.mode {
SsrMode::Static(ref route) => Some(route),
_ => None,
}
}
pub fn into_static_parts(self) -> Option<(StaticMode, StaticDataMap)> {
self.static_mode
pub async fn into_static_paths(self) -> Option<Vec<ResolvedStaticPath>> {
let params = self.static_route()?.to_prerendered_params().await;
Some(StaticPath::new(self.path).into_paths(params))
}
pub async fn generate_static_files<Fut, WriterFut>(
mut self,
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
+ Send
+ Clone
+ 'static,
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
) where
Fut: Future<Output = (Owner, String)> + Send + 'static,
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
{
if let SsrMode::Static(_) = self.mode() {
let (all_initial_tx, all_initial_rx) = std::sync::mpsc::channel();
let render_fn = render_fn.clone();
let regenerate = mem::take(&mut self.regenerate);
let paths = self.into_static_paths().await.unwrap_or_default();
for path in paths {
// Err(_) here would just mean they've dropped the rx and are no longer awaiting
// it; we're only using it to notify them it's done so it doesn't matter in that
// case
_ = all_initial_tx.send(path.build(
render_fn.clone(),
writer.clone(),
was_404.clone(),
regenerate.clone(),
));
}
join_all(all_initial_rx.try_iter()).await;
}
}
/*
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
/// is not marked as statically rendered. All route parameters to use when resolving all paths
/// to render should be passed in the `params` argument.
pub async fn build_static<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Send + 'static + Clone,
additional_context: impl Fn() + Send + 'static + Clone,
params: &StaticParamsMap,
) -> Result<bool, std::io::Error>
where
IV: IntoView + 'static,
{
match self.static_mode {
None => Ok(false),
Some(_) => {
let mut path = StaticPath::new(&self.leptos_path);
path.add_params(params);
for path in path.into_paths() {
path.write(
options,
app_fn.clone(),
additional_context.clone(),
)
.await?;
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
/// is not marked as statically rendered. All route parameters to use when resolving all paths
/// to render should be passed in the `params` argument.
pub async fn build_static<IV>(
&self,
options: &LeptosOptions,
app_fn: impl Fn() -> IV + Send + 'static + Clone,
additional_context: impl Fn() + Send + 'static + Clone,
params: &StaticParamsMap,
) -> Result<bool, std::io::Error>
where
IV: IntoView + 'static,
{
match self.mode {
SsrMode::Static(route) => {
let mut path = StaticPath::new(self.path.clone());
for path in path.into_paths(params) {
/*path.write(
options,
app_fn.clone(),
additional_context.clone(),
)
.await?;*/ println!()
}
Ok(true)
}
Ok(true)
_ => Ok(false),
}
}
}*/
*/
}
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub struct RouteList(Vec<RouteListing>);
impl From<Vec<RouteListing>> for RouteList {
@@ -124,6 +171,45 @@ impl RouteList {
pub fn into_inner(self) -> Vec<RouteListing> {
self.0
}
pub fn iter(&self) -> impl Iterator<Item = &RouteListing> {
self.0.iter()
}
pub async fn into_static_paths(self) -> Vec<ResolvedStaticPath> {
futures::future::join_all(
self.into_inner()
.into_iter()
.map(|route_listing| route_listing.into_static_paths()),
)
.await
.into_iter()
.flatten()
.flatten()
.collect::<Vec<_>>()
}
pub async fn generate_static_files<Fut, WriterFut>(
self,
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
+ Send
+ Clone
+ 'static,
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
) where
Fut: Future<Output = (Owner, String)> + Send + 'static,
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
{
join_all(self.into_inner().into_iter().map(|route| {
route.generate_static_files(
render_fn.clone(),
writer.clone(),
was_404.clone(),
)
}))
.await;
}
}
impl RouteList {

View File

@@ -155,8 +155,10 @@ pub fn use_location() -> Location {
location
}
pub(crate) type RawParamsMap = ArcMemo<ParamsMap>;
#[track_caller]
fn use_params_raw() -> ArcMemo<ParamsMap> {
fn use_params_raw() -> RawParamsMap {
use_context().expect(
"Tried to access params outside the context of a matched <Route>.",
)

View File

@@ -16,7 +16,7 @@ pub mod nested_router;
pub mod params;
//mod router;
mod ssr_mode;
mod static_route;
pub mod static_routes;
pub use generate_route_list::*;
#[doc(inline)]
@@ -26,4 +26,3 @@ pub use method::*;
pub use navigate::*;
//pub use router::*;
pub use ssr_mode::*;
pub use static_route::*;

View File

@@ -201,10 +201,29 @@ where
}
}
#[cfg(feature = "ssr")]
pub(crate) fn unescape(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap()
.to_string()
}
#[cfg(not(feature = "ssr"))]
pub(crate) fn unescape(s: &str) -> String {
js_sys::decode_uri_component(s).unwrap().into()
}
#[cfg(not(feature = "ssr"))]
pub(crate) fn unescape_minimal(s: &str) -> String {
js_sys::decode_uri(s).unwrap().into()
}
#[cfg(feature = "ssr")]
pub(crate) fn unescape_minimal(s: &str) -> String {
unescape(s)
}
pub(crate) fn handle_anchor_click<NavFn, NavFut>(
router_base: Option<Cow<'static, str>>,
parse_with_base: fn(&str, &str) -> Result<Url, JsValue>,
@@ -259,7 +278,7 @@ where
}
let url = parse_with_base(href.as_str(), &origin).unwrap();
let path_name = unescape(&url.path);
let path_name = unescape_minimal(&url.path);
// let browser handle this event if it leaves our domain
// or our base path

View File

@@ -6,10 +6,10 @@ pub use path_segment::*;
mod horizontal;
mod nested;
mod vertical;
use crate::SsrMode;
use crate::{static_routes::RegenerationFn, Method, SsrMode};
pub use horizontal::*;
pub use nested::*;
use std::{borrow::Cow, marker::PhantomData};
use std::{borrow::Cow, collections::HashSet, marker::PhantomData};
use tachys::{
renderer::Renderer,
view::{Render, RenderHtml},
@@ -145,6 +145,8 @@ where
pub struct GeneratedRouteData {
pub segments: Vec<PathSegment>,
pub ssr_mode: SsrMode,
pub methods: HashSet<Method>,
pub regenerate: Vec<RegenerationFn>,
}
#[cfg(test)]

View File

@@ -2,11 +2,12 @@ use super::{
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
PossibleRouteMatch, RouteMatchId,
};
use crate::{ChooseView, GeneratedRouteData, MatchParams, SsrMode};
use crate::{ChooseView, GeneratedRouteData, MatchParams, Method, SsrMode};
use core::{fmt, iter};
use either_of::Either;
use std::{
borrow::Cow,
collections::HashSet,
marker::PhantomData,
sync::atomic::{AtomicU16, Ordering},
};
@@ -19,7 +20,7 @@ mod tuples;
static ROUTE_ID: AtomicU16 = AtomicU16::new(1);
#[derive(Debug, Copy, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub struct NestedRoute<Segments, Children, Data, View, R> {
id: u16,
segments: Segments,
@@ -27,6 +28,7 @@ pub struct NestedRoute<Segments, Children, Data, View, R> {
data: Data,
view: View,
rndr: PhantomData<R>,
methods: HashSet<Method>,
ssr_mode: SsrMode,
}
@@ -46,7 +48,8 @@ where
data: self.data.clone(),
view: self.view.clone(),
rndr: PhantomData,
ssr_mode: self.ssr_mode,
methods: self.methods.clone(),
ssr_mode: self.ssr_mode.clone(),
}
}
}
@@ -64,6 +67,7 @@ impl<Segments, View, R> NestedRoute<Segments, (), (), View, R> {
data: (),
view,
rndr: PhantomData,
methods: [Method::Get].into(),
ssr_mode: Default::default(),
}
}
@@ -81,6 +85,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
view,
rndr,
ssr_mode,
methods,
..
} = self;
NestedRoute {
@@ -90,6 +95,7 @@ impl<Segments, Data, View, R> NestedRoute<Segments, (), Data, View, R> {
data,
view,
ssr_mode,
methods,
rndr,
}
}
@@ -249,25 +255,44 @@ where
let mut segment_routes = Vec::new();
self.segments.generate_path(&mut segment_routes);
let children = self.children.as_ref();
let ssr_mode = self.ssr_mode;
let ssr_mode = self.ssr_mode.clone();
let methods = self.methods.clone();
let regenerate = match &ssr_mode {
SsrMode::Static(data) => match data.regenerate.as_ref() {
None => vec![],
Some(regenerate) => vec![regenerate.clone()]
}
_ => vec![]
};
match children {
None => Either::Left(iter::once(GeneratedRouteData {
segments: segment_routes,
ssr_mode
ssr_mode,
methods,
regenerate
})),
Some(children) => {
Either::Right(children.generate_routes().into_iter().map(move |child| {
// extend this route's segments with child segments
let segments = segment_routes.clone().into_iter().chain(child.segments).collect();
let mut methods = methods.clone();
methods.extend(child.methods);
let mut regenerate = regenerate.clone();
regenerate.extend(child.regenerate);
if child.ssr_mode > ssr_mode {
GeneratedRouteData {
segments,
ssr_mode: child.ssr_mode,
methods, regenerate
}
} else {
GeneratedRouteData {
segments,
ssr_mode,
ssr_mode: ssr_mode.clone(), methods, regenerate
}
}
}))

View File

@@ -3,8 +3,8 @@ use crate::{
location::{LocationProvider, Url},
matching::Routes,
params::ParamsMap,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, Method,
PathSegment, RouteList, RouteListing, RouteMatchId,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
RouteList, RouteListing, RouteMatchId,
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
@@ -272,10 +272,8 @@ where
RouteListing::new(
path,
data.ssr_mode,
// TODO methods
[Method::Get],
// TODO static data
None,
data.methods,
data.regenerate,
)
})
.collect::<Vec<_>>();

View File

@@ -1,3 +1,5 @@
use crate::static_routes::StaticRoute;
/// Indicates which rendering mode should be used for this route during server-side rendering.
///
/// Leptos supports the following ways of rendering HTML that contains `async` data loaded
@@ -18,15 +20,17 @@
/// 5. **`Async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
/// 6. **`Static`**:
///
/// The mode defaults to out-of-order streaming. For a path that includes multiple nested routes, the most
/// restrictive mode will be used: i.e., if even a single nested route asks for `Async` rendering, the whole initial
/// request will be rendered `Async`. (`Async` is the most restricted requirement, followed by `InOrder`, `PartiallyBlocked`, and `OutOfOrder`.)
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SsrMode {
#[default]
OutOfOrder,
PartiallyBlocked,
InOrder,
Async,
Static(StaticRoute),
}

View File

@@ -1,21 +0,0 @@
/// The mode to use when rendering the route statically.
/// On mode `Upfront`, the route will be built with the server is started using the provided static
/// data. On mode `Incremental`, the route will be built on the first request to it and then cached
/// and returned statically for subsequent requests.
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum StaticMode {
#[default]
Upfront,
Incremental,
}
// TODO
#[derive(Debug, Clone)]
pub struct StaticDataMap;
impl StaticDataMap {
#[allow(clippy::new_without_default)] // TODO
pub fn new() -> Self {
Self
}
}

363
router/src/static_routes.rs Normal file
View File

@@ -0,0 +1,363 @@
use crate::{hooks::RawParamsMap, params::ParamsMap, PathSegment};
use futures::{channel::oneshot, stream, Stream, StreamExt};
use leptos::spawn::spawn;
use reactive_graph::{owner::Owner, traits::GetUntracked};
use std::{
fmt::{Debug, Display},
future::Future,
ops::Deref,
pin::Pin,
sync::Arc,
};
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
pub type StaticParams = Arc<StaticParamsFn>;
pub type StaticParamsFn =
dyn Fn() -> PinnedFuture<StaticParamsMap> + Send + Sync + 'static;
#[derive(Clone)]
#[allow(clippy::type_complexity)]
pub struct RegenerationFn(
Arc<dyn Fn(&ParamsMap) -> PinnedStream<()> + Send + Sync>,
);
impl Debug for RegenerationFn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegenerationFn").finish_non_exhaustive()
}
}
impl Deref for RegenerationFn {
type Target = dyn Fn(&ParamsMap) -> PinnedStream<()> + Send + Sync;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl PartialEq for RegenerationFn {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Clone, Default)]
pub struct StaticRoute {
pub(crate) prerender_params: Option<StaticParams>,
pub(crate) regenerate: Option<RegenerationFn>,
}
impl StaticRoute {
pub fn new() -> Self {
Self::default()
}
pub fn prerender_params<Fut>(
mut self,
params: impl Fn() -> Fut + Send + Sync + 'static,
) -> Self
where
Fut: Future<Output = StaticParamsMap> + Send + 'static,
{
self.prerender_params = Some(Arc::new(move || Box::pin(params())));
self
}
pub fn regenerate<St>(
mut self,
invalidate: impl Fn(&ParamsMap) -> St + Send + Sync + 'static,
) -> Self
where
St: Stream<Item = ()> + Send + 'static,
{
self.regenerate = Some(RegenerationFn(Arc::new(move |params| {
Box::pin(invalidate(params))
})));
self
}
pub async fn to_prerendered_params(&self) -> Option<StaticParamsMap> {
match &self.prerender_params {
None => None,
Some(params) => Some(params().await),
}
}
}
impl Debug for StaticRoute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticRoute").finish_non_exhaustive()
}
}
impl PartialOrd for StaticRoute {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StaticRoute {
fn cmp(&self, _other: &Self) -> std::cmp::Ordering {
std::cmp::Ordering::Equal
}
}
impl PartialEq for StaticRoute {
fn eq(&self, other: &Self) -> bool {
let prerender = match (&self.prerender_params, &other.prerender_params)
{
(None, None) => true,
(None, Some(_)) | (Some(_), None) => false,
(Some(this), Some(that)) => Arc::ptr_eq(this, that),
};
prerender && (self.regenerate == other.regenerate)
}
}
impl Eq for StaticRoute {}
#[derive(Debug, Clone, Default)]
pub struct StaticParamsMap(pub Vec<(String, Vec<String>)>);
impl StaticParamsMap {
/// Create a new empty `StaticParamsMap`.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Insert a value into the map.
#[inline]
pub fn insert(&mut self, key: impl ToString, value: Vec<String>) {
let key = key.to_string();
for item in self.0.iter_mut() {
if item.0 == key {
item.1 = value;
return;
}
}
self.0.push((key, value));
}
/// Get a value from the map.
#[inline]
pub fn get(&self, key: &str) -> Option<&Vec<String>> {
self.0
.iter()
.find_map(|entry| (entry.0 == key).then_some(&entry.1))
}
}
impl IntoIterator for StaticParamsMap {
type Item = (String, Vec<String>);
type IntoIter = StaticParamsIter;
fn into_iter(self) -> Self::IntoIter {
StaticParamsIter(self.0.into_iter())
}
}
#[derive(Debug)]
pub struct StaticParamsIter(
<Vec<(String, Vec<String>)> as IntoIterator>::IntoIter,
);
impl Iterator for StaticParamsIter {
type Item = (String, Vec<String>);
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl<A> FromIterator<A> for StaticParamsMap
where
A: Into<(String, Vec<String>)>,
{
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
Self(iter.into_iter().map(Into::into).collect())
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct StaticPath {
segments: Vec<PathSegment>,
}
impl StaticPath {
pub fn new(segments: Vec<PathSegment>) -> StaticPath {
Self { segments }
}
pub fn into_paths(
self,
params: Option<StaticParamsMap>,
) -> Vec<ResolvedStaticPath> {
use PathSegment::*;
let mut paths = vec![ResolvedStaticPath {
path: String::new(),
}];
for segment in &self.segments {
match segment {
Unit => {}
Static(s) => {
paths = paths
.into_iter()
.map(|p| {
if s.starts_with("/") {
ResolvedStaticPath {
path: format!("{}{s}", p.path),
}
} else {
ResolvedStaticPath {
path: format!("{}/{s}", p.path),
}
}
})
.collect::<Vec<_>>();
}
Param(name) | Splat(name) => {
let mut new_paths = vec![];
if let Some(params) = params.as_ref() {
for path in paths {
if let Some(params) = params.get(name) {
for val in params.iter() {
new_paths.push(if val.starts_with("/") {
ResolvedStaticPath {
path: format!(
"{}{}",
path.path, val
),
}
} else {
ResolvedStaticPath {
path: format!(
"{}/{}",
path.path, val
),
}
});
}
}
}
}
paths = new_paths;
}
}
}
paths
}
}
#[derive(Debug, Clone)]
pub struct ResolvedStaticPath {
pub(crate) path: String,
}
impl ResolvedStaticPath {
pub fn new(path: impl Into<String>) -> Self {
Self { path: path.into() }
}
}
impl AsRef<str> for ResolvedStaticPath {
fn as_ref(&self) -> &str {
self.path.as_ref()
}
}
impl Display for ResolvedStaticPath {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Display::fmt(&self.path, f)
}
}
impl ResolvedStaticPath {
pub async fn build<Fut, WriterFut>(
self,
render_fn: impl Fn(&ResolvedStaticPath) -> Fut + Send + Clone + 'static,
writer: impl Fn(&ResolvedStaticPath, &Owner, String) -> WriterFut
+ Send
+ Clone
+ 'static,
was_404: impl Fn(&Owner) -> bool + Send + Clone + 'static,
regenerate: Vec<RegenerationFn>,
) -> (Owner, Option<String>)
where
Fut: Future<Output = (Owner, String)> + Send + 'static,
WriterFut: Future<Output = Result<(), std::io::Error>> + Send + 'static,
{
let (tx, rx) = oneshot::channel();
// spawns a separate task for each path it's rendering
// this allows us to parallelize all static site rendering,
// and also to create long-lived tasks
spawn({
let render_fn = render_fn.clone();
let writer = writer.clone();
let was_error = was_404.clone();
async move {
// render and write the initial page
let (owner, html) = render_fn(&self).await;
// if rendering this page resulted in an error (404, 500, etc.)
// then we should not cache it: the `was_error` function can handle notifying
// the user that there was an error, and the server can give a dynamic response
// that will include the 404 or 500
if was_error(&owner) {
// can ignore errors from channel here, because it just means we're not
// awaiting the Future
_ = tx.send((owner.clone(), Some(html)));
} else {
_ = tx.send((owner.clone(), None));
if let Err(e) = writer(&self, &owner, html).await {
#[cfg(feature = "tracing")]
tracing::warn!("{e}");
#[cfg(not(feature = "tracing"))]
eprintln!("{e}");
}
}
// if there's a regeneration function, keep looping
let params = if regenerate.is_empty() {
None
} else {
Some(
owner
.use_context_bidirectional::<RawParamsMap>()
.expect(
"using static routing, but couldn't find \
ParamsMap",
)
.get_untracked(),
)
};
let mut regenerate = stream::select_all(
regenerate
.into_iter()
.map(|r| owner.with(|| r(params.as_ref().unwrap()))),
);
while regenerate.next().await.is_some() {
let (owner, html) = render_fn(&self).await;
if !was_error(&owner) {
if let Err(e) = writer(&self, &owner, html).await {
#[cfg(feature = "tracing")]
tracing::warn!("{e}");
#[cfg(not(feature = "tracing"))]
eprintln!("{e}");
}
}
drop(owner);
}
}
});
rx.await.unwrap()
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.7.0-beta2"
version = "0.7.0-beta4"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -18,4 +18,4 @@ proc-macro2 = "1.0"
quote = "1.0"
[dev-dependencies]
leptos_router = { workspace = true }
leptos_router = { version = "0.7.0-beta" }

View File

@@ -75,6 +75,9 @@ impl SegmentParser {
lit.trim_start_matches(['"', '/'])
.trim_end_matches(['"', '/']),
);
if lit.ends_with(r#"/""#) && lit != r#""/""# {
self.segments.push(Segment::Static("/".to_string()));
}
}
TokenTree::Group(_) => unimplemented!(),
TokenTree::Ident(_) => unimplemented!(),
@@ -102,13 +105,14 @@ impl SegmentParser {
impl Segment {
fn is_valid(segment: &str) -> bool {
segment.chars().all(|c| {
c.is_ascii_digit()
|| c.is_ascii_lowercase()
|| c.is_ascii_uppercase()
|| RFC3986_UNRESERVED.contains(&c)
|| RFC3986_PCHAR_OTHER.contains(&c)
})
segment == "/"
|| segment.chars().all(|c| {
c.is_ascii_digit()
|| c.is_ascii_lowercase()
|| c.is_ascii_uppercase()
|| RFC3986_UNRESERVED.contains(&c)
|| RFC3986_PCHAR_OTHER.contains(&c)
})
}
fn ensure_valid(&self) {

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