Native Shell
Native Shell:AI 生成的 UI,在你的应用中原生呈现,覆盖每个平台。iOS、Android、macOS、Windows 与 Linux 上的应用、GPUI 应用与网页在运行时加载已签名、沙箱隔离的 TSX 模块并绘制 th
AI 翻译
Server-driven hypermedia for Rust where signal names, attribute expressions, actions, and patch targets are type-checked
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Server-owned state, type-checked strings.
hypersig is a hypermedia web framework on axum in which the page, its behavior in the browser, and the server handlers are all written in Rust. The server holds the state and sends the browser patches. Every string the browser acts on (signal names, expressions, action URLs, links) is produced from a Rust type, so a mistake in one is a compile error.
cargo build builds the whole application, with no Node or wasm step.Add the dependencies to Cargo.toml:
[dependencies]
hypersig = "0.15"
axum = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
Write src/main.rs:
use axum::Router;
use hypersig::html::{button, div, span};
use hypersig::{Action, Page, Patches, Route, RouterExt, Signals};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
}
hypersig::routes! {
fn pages(Router<()>);
const INDEX: Route = "/" => index;
action RESET: Action<Counter> = post "/reset" => reset;
}
async fn index((): (), (): ()) -> Page<Counter> {
let count = Counter::count();
Page::new("Counter", Counter { count: 0 }).child(
div()
.child(button().on_click(count.set(count + 1)).text("+1"))
.child(span().bind_text(count.to_text()))
.child(button().on_click(RESET.call()).text("Reset on the server")),
)
}
async fn reset((): (), (): ()) -> Patches<Counter> {
Counter::count().patch(0).into()
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let app = pages(Router::new()).hypersig_runtime();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, app).await
}
Run cargo run and open http://127.0.0.1:3000. The +1 button counts in the browser without a request, and the reset button calls /reset, whose patch sets count back to 0. Misspelling count, or adding a string to it, stops cargo build. With the CLI (cargo install --locked hypersig_cli), cargo hypersig new my-app starts a project and cargo hypersig dev rebuilds and reloads it after each change, building with blaze, which it needs installed. The documentation continues from here.
Why hypersig compares hypersig with Topcoat and Datastar, which come closest, in more detail.
| hypersig | Topcoat | React | Next.js | TanStack Start | Nuxt | Datastar | Leptos | Dioxus | |
|---|---|---|---|---|---|---|---|---|---|
| Page written in | Rust | Rust | TypeScript | TypeScript | TypeScript | TypeScript and Vue templates | HTML attributes | Rust | Rust |
| Server written in | Rust | Rust | Any | TypeScript | TypeScript | TypeScript | Any | Rust | Rust |
| No browser build | Yes | Yes | No (Vite) | No (Node bundler) | No (Vite) | No (Vite) | Yes | No (cargo leptos, to Wasm) | No (dx, to Wasm) |
| Request types checked at compile time | Yes | Yes | No | Yes | Yes | No | No | Yes | Yes |
| Links checked against routes at compile time | Yes | Handler only (parameters at run time) | No (no router) | Opt-in (typedRoutes) | Yes | Experimental (typedPages) | No | No (href is a string) | Yes (a Routable enum value) |
| State kept in the browser | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
The test app is a 2000-item TodoMVC that stores every add, toggle, and delete on the server. Each number is the median of 10 runs in Chrome 155 on an Apple M4 Max, with the CPU slowed 4x and 50 ms added to every request, and the fastest and slowest of the 10 runs are in parentheses.
| hypersig | Topcoat | React | Next.js | TanStack Start | Nuxt | Datastar | Leptos | Dioxus | |
|---|---|---|---|---|---|---|---|---|---|
| JS (gzip) | 19.7 KB | 14.8 KB | 67.1 KB | 133.3 KB | 119.5 KB | 59.2 KB | 13.1 KB | 5.5 KB | 15.7 KB |
| Wasm (gzip) | - | - | - | - | - | - | - | 145.1 KB | 441.4 KB |
| First Contentful Paint | 239 ms (231–309) | 221 ms (206–241) | 209 ms (198–266) | 240 ms (229–261) | 237 ms (217–251) | 236 ms (223–263) | 217 ms (204–249) | 232 ms (222–240) | 213 ms (205–244) |
| Total Blocking Time | 3 ms (0–37) | 318 ms (311–351) | 16 ms (11–23) | 14 ms (9–16) | 0 ms (0–0) | 36 ms (29–42) | 20 ms (17–23) | 47 ms (42–57) | 122 ms (114–126) |
| Interaction to Next Paint | 24 ms (24–24) | 24 ms (24–24) | 24 ms (16–24) | 24 ms (16–24) | 24 ms (16–24) | 24 ms (24–24) | 16 ms (16–16) | 24 ms (16–24) | 72 ms (72–80) |
The apps and the harness are in bench/compare, measured at commit 7d031d0. Leptos and Dioxus load most of their code as WebAssembly, which the Wasm row counts. bench/js-framework-benchmark has the results of js-framework-benchmark, and the hypersig_style README compares the styling crates. On the same machine, examples/todomvc (485 lines) builds from an empty target directory in 13.4 s and rebuilds after a one-line change in 1.3 s (cargo build, Rust 1.99.0).
cargo hypersig new my-app --host server|cloudflare|vercel writes a project for a container host, Cloudflare Workers, or a Vercel Function, and cargo hypersig add <host> adds one to an existing project (Deploying). Security lists what hypersig checks on each request and what each application checks itself, starting with who may call each action.
The compiler checks that names, types, and links match. These mistakes still compile, and show up only when the program runs. The documentation has the same list.
| Mistake | What happens |
|---|---|
The function routes! generates is never passed to the router | Its pages and actions answer 404 |
A block under a Base is served neither inside Router::nest at that base nor behind a proxy that strips it | Its pages and actions answer 404, since its links start with the base and its routes do not |
A page under a Base calls an action of a routes! block without it | The call answers 404 |
One node_ref!, dialog_ref!, Id, or FileInput name goes on two elements, or El::id is used inside a list template | Rendering the page panics. Two ids from Id::from_text alike are not caught, and a reference to that id reaches the first |
label_for or a similar reference names an Id no element carries | The control has no label |
A script outside hypersig finds an El::foreign element by its value, and the constant is renamed | The script finds no element. Look it up by Id instead |
A #[signals(computed)] field is never passed to Page::computed | Rendering the page panics, for a field of a nested struct only where the page reads it |
| A list item is used outside the closure it was passed to | Building or rendering the page panics |
| A signal holds an integer beyond ±2^53 | Rendering the page or building the patch panics |
Without the server, the browser runs only the operations hypersig provides and the scripts a page loads. A JavaScript library such as a map or an editor runs inside a custom element, and cargo hypersig elements writes the names from its custom-elements.json as Rust items (JavaScript libraries). hypersig is a poor fit for:
CONTRIBUTING.md covers bug reports and pull requests, and AGENTS.md describes the repository layout and the checks a change runs.
MIT