autumn-plugin-leptos
Leptos islands for Autumn 0.8 apps. Put Leptos 0.8 components (Rust, compiled to wasm) into Maud + htmx pages.
- The plugin serves a small loader and your wasm bundle through the Autumn
plugin_assetsAPI: hashed URLs,immutablecache, SRI. The browser checks the SRI hash of the glue module and of the.wasmfile. - The client crate registers your components. You write no JavaScript.
- Props are typed
serdestructs. The component gets them as aSignal<P>. New props from the server keep the component state. - The loader mounts islands that htmx swaps in, and unmounts islands that htmx swaps out. Leptos cleanup runs.
- A Rust panic affects only its own bundle. The islands of that bundle show their fallback again.
- With the
ssrfeature, the server renders the same component as the fallback.
Install
The server crate:
[]
= { = "0.3", = ["ssr"] }
= "0.10"
= "0.8" # for `fallback_view` and the shared components
= { = "1", = ["derive"] }
= "1"
The wasm crate ([lib] crate-type = ["cdylib"]):
[]
= "0.1"
= { = "0.8", = ["csr"] }
= { = "1", = ["derive"] }
= "=0.2.129"
Install the wasm tools one time. The wasm-bindgen-cli version must be
the wasm-bindgen version in the Cargo.lock of the wasm crate:
Use
1. Write and register the components
// islands/src/lib.rs
use register;
use *;
use ;
use *;
- Without
data-leptos-props, the props are{}. For a component with no props, use()or an empty struct as the props type. #[component] fn Countermakes aCounterPropstype. Do not give your props struct the same name.- Register a name in one bundle only. When two bundles register one name, the bundle that loads first gets the name.
2. Build the bundle
The output is a glue module (islands.js) and a wasm module
(islands_bg.wasm). Keep both in one bundle.
examples/islands-app/build.sh is a
complete script.
3. Install the plugin and render islands
use ;
use ;
use *;
static ISLANDS: PluginAssets = from_files;
async
async
- Give the same bundle to
LeptosPlugin::bundleandleptos_bundle. leptos_bundlerenders a<link rel="modulepreload">for the glue and each snippet, and a<link rel="preload">for the wasm. The loader imports the glue.Island::inline()renders a<span>.Island::attradds attributes such asroleoraria-label. It refusesid,class,data-leptos-*,on*,hx-on*anddata-hx-on*.- A bundle with no
x.js+x_bg.wasmpair, or with the namespaceleptos, stops the app at start-up.
4. Allow wasm in the CSP
A browser compiles wasm only when script-src has
'wasm-unsafe-eval'. This token does not allow eval(). The Autumn
default CSP does not have it. Use one of these:
WasmCspLoader(above) adds the token to the policy fromautumn.tomlandAUTUMN_*variables. Wrap your own loader withWasmCspLoader::new(loader).- Put
wasm_csp()(the default policy plus the token) insecurity.headers.content_security_policy. add_wasm_unsafe_eval(policy)adds the token to any policy.
Autumn adds nonces (security.headers.csp_nonce) only to its default
policy. A changed policy has no nonces. Because of this, WasmCspLoader
stops with an error when nonce mode is on. Do not put the nonce placeholder
AUTUMN_CSP_NONCE in your own policy: Autumn then sends that fixed text
as the nonce.
Without the token, the islands keep their fallback.
5. Send new props from the server
use PropsUpdate;
async
PropsUpdate writes the HX-Trigger header. htmx sends the
autumn:leptos:props event. The loader sets data-leptos-props, and the
client sets the props signal of the same instance.
- htmx handles
HX-Triggerbefore the swap. To update an island that the same response swaps in, use.after_settle(). - Each selector selects the first matching element. If that element is not an island, the loader ignores the update.
- Build selectors from trusted values. Keep header props small.
App code can send the same event:
island.;
6. Render the fallback on the server (ssr)
let counter = new
.props?
.fallback_view;
- Share the component file between the server and the wasm crate. The
demo uses
#[path]. - Make signals inside the closure. A signal that you make outside it is never released.
- Use synchronous views only. A resource or
spawn_localneeds an async runtime thatfallback_viewdoes not give.
The server HTML shows before the wasm loads. The loader replaces it with the live component. There is no hydration.
The island element
fallback
| Attribute | Set by | Meaning |
|---|---|---|
data-leptos-island |
Island::new |
The registered name. A change mounts a new instance. |
data-leptos-props |
Island::props |
A JSON object. A change sets the props signal. |
data-leptos-mount |
Island::mount_when |
idle or visible. No attribute: mount at load. |
data-leptos-state |
the loader | waiting, pending (a bundle still loads), mounted or error. |
data-leptos-ignore |
you | No island and no bundle link in this element loads. |
The loader sends these events. They bubble from the island. An island
that left the page sends unmount on document. detail has name and
element (and error for error):
| Event | When |
|---|---|
autumn:leptos:mount |
The component mounts. |
autumn:leptos:update |
New props are in the DOM. |
autumn:leptos:unmount |
The component unmounts. |
autumn:leptos:error |
Bad props, a mount error, a panic, or no loaded bundle has the name. A bundle that cannot load sends it on document with detail.bundle. |
window.autumnLeptos.names() and window.autumnLeptos.bundles() show
the registered names and the bundle states (loading, ready, failed,
crashed).
Behavior
- Load. The loader reads each
link[data-leptos-wasm]. It refuses a link that is notrel="modulepreload", has no SRI hash, is not same-origin, or is in[data-leptos-ignore]or an island. It imports the glue module. It givesinitafetchof the.wasmwith its SRI hash. Then it checks the ABI version of the client crate and reads the names. - Mount. The loader moves the fallback out of the island and keeps a
copy in a
<template data-leptos-fallback>. Then the client calls Leptosmount_to. - Unknown name. The island is
pendingwhile a bundle loads. When no bundle loads and none has the name, the island getserrorand keeps its fallback. A later bundle can still mount it. - Errors. Bad props or a failed mount keep the fallback. Bad props on a mounted island keep the last good render. A bundle that cannot load (import error, SRI mismatch, ABI mismatch, CSP) writes a console error. Each error affects one island or one bundle only.
- Panics. A Rust panic leaves the wasm instance in an unknown state.
The client panic hook calls a private callback of the loader. Each
island of that bundle shows its fallback and gets
error. The loader makes no more calls into the bundle, and the client refuses new work. Timers and listeners that Leptos installed can still run. Split bundles to isolate components. - htmx. One
MutationObservermounts added islands and unmounts removed islands. An island that moves in one task keeps its instance. - History. An htmx history snapshot has the old output and the kept fallback copy. After a restore, the loader puts the copy back and mounts the island again.
- Nesting. An island inside another island does not mount.
Security
- User HTML. The loader mounts each
data-leptos-islandelement in the page and imports each accepted bundle link. If your app shows user HTML, the sanitizer must removedata-leptos-*,hx-*anddata-hx-*attributes and<link>and<template>elements.data-leptos-ignoredoes not give full protection. An htmx out-of-band swap can move an element out of it. - Props. Props are in an HTML attribute. Maud escapes them. Rust
serdereads them into your type. Do not put secrets in props. - Wasm.
'wasm-unsafe-eval'allows wasm compilation only. An island runs as first-party code. It can do all the operations that the page can do. Do not use islands for untrusted code. - Attributes. Maud escapes each
Island::attrvalue. Some values are code or URLs (hx-get,hx-vals,style). Do not put user input in them. - Updates. Any same-origin script and any
HX-Triggerheader can sendautumn:leptos:props. Do not copy user input into a trigger header. - Names. A library bundle must use a prefix in its names
(
Acme.Chart).
Demo
Open http://127.0.0.1:3000. The demo has a counter with an SSR fallback
and a props update, a clock that htmx adds and removes, and a panic
button. The repository contains the bundle in examples/islands/. You do
not need a wasm toolchain to run the demo. Rebuild the bundle with
examples/islands-app/build.sh.
Development
The browser tests need Chrome or Chromium on PATH, or the binary path in
AUTUMN_CHROMIUM.
License
Apache-2.0.