Implement closed typed effect path
This commit is contained in:
@@ -1,210 +1,94 @@
|
||||
# hemx
|
||||
# Hemx
|
||||
|
||||
hemx is checked hypermedia for Rust: write hemplate templates, write typed Rust
|
||||
handlers, and return generated UI commands. The browser receives checked UI
|
||||
commands; ordinary server-first apps do not need a frontend framework,
|
||||
handwritten UI JavaScript, selector targeting, or raw runtime primitives. req: pitch/001 req: canonical_authoring/001
|
||||
Hemx is checked hypermedia for Rust. Applications render Hemplate views, handle
|
||||
events in typed Rust functions, and return generated UI effects. The browser runs
|
||||
a small effect interpreter instead of a virtual DOM, hydration framework, or
|
||||
client-side expression language. The same core compiles for native servers and
|
||||
server-side Wasm isolates; platform storage, sockets, and lifecycle remain
|
||||
application concerns.
|
||||
|
||||
Status: the evidence-backed v1 behavior slices are implemented and browser-proven:
|
||||
server-first/page-enhanced behavior, client-local WASM, durable offline/sync, the
|
||||
multiplayer Kanban milestone, and the production reference. Local workspace,
|
||||
browser, performance, documentation, and canonical-example gates pass. The
|
||||
warning-denied vulnerability and source audits are clean; strict license closure
|
||||
awaits a repository license allowlist. See `docs/v1-product-evidence.md` for the
|
||||
product boundary, `REQUIREMENTS.md` for authority, `PLAN.md` for execution state,
|
||||
and `docs/v1-readiness.md` for evidence. No publishing is implied.
|
||||
## How it works
|
||||
|
||||
Template authoring: `.heml` is HTML plus a small hemplate overlay for escaped
|
||||
text, trusted HTML, dynamic attributes, Rust-shaped control directives, generated
|
||||
slots/forms/handles, and keyed partial targets. See `docs/hemplate-syntax.md`.
|
||||
Editor setup for VS Code, Cursor, and Neovim lives in `docs/editor-support.md`;
|
||||
VS Code/Cursor share the repo extension in `editors/vscode-hemx`, while all
|
||||
editors keep normal HTML/tree-sitter highlighting and layer `hemx-build`
|
||||
diagnostics on top.
|
||||
1. `.heml` templates declare page roots, slots, forms, handles, and keyed targets.
|
||||
2. `hemx-build` generates typed Rust helpers from that surface.
|
||||
3. `#[hemx::handler]` functions accept ordinary Rust inputs and return typed effects.
|
||||
4. `hemx-axum` serves pages, assets, handler routes, and effect responses.
|
||||
5. The browser runtime validates the build fingerprint and applies effects within the current root.
|
||||
|
||||
Local checkout note: until the hemplate crates are published, this repository
|
||||
expects `hemplate` checked out next to `hemx` as `../hemplate/hemplate`. The app
|
||||
scaffolder fails with that exact path if the prerequisite is missing, instead of
|
||||
creating an app that fails later with a vague Cargo path-dependency error.
|
||||
|
||||
## The normal path
|
||||
|
||||
For beginner and production-shaped app code, stay on this path. req: public_api/001 req: public_api/005
|
||||
|
||||
1. **Templates declare the surface.** `.heml` files declare roots, slots,
|
||||
forms, handles, keys, page targets, optional pending states, and explicit
|
||||
leaf islands with `data-hemx-*` attributes. The stable syntax surface lives in
|
||||
`docs/hemplate-syntax.md`.
|
||||
2. **Build generates typed helpers.** `hemx_build::app().run()` consumes the
|
||||
hemplate Surface and emits generated Rust helpers for slots, forms, handles,
|
||||
page targets, classes, and events. req: ceremony/003 req: build/001
|
||||
3. **Handlers are plain Rust.** App code uses `#[hemx::handler]` functions with
|
||||
ordinary domain types, framework extractors, generated `Form<T>` inputs,
|
||||
explicit `data-*` params, and `impl IntoEffect` or fallible returns. req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005
|
||||
4. **Handlers return generated commands.** Common handlers return helpers such
|
||||
as `todos.append(todo)`, `todo_row.replace(row)`, `new_todo.error("title",
|
||||
"Required")`, `new_todo.clear()`, `page.replace(view)`, or tuples of those
|
||||
commands. req: dx/006 req: dx/007
|
||||
5. **The runtime only applies effects.** The JavaScript runtime is a small,
|
||||
root-scoped effect interpreter: no VDOM, no hydration framework, no client
|
||||
expression engine, no selector retargeting, and no app state store. req: runtime/003 req: invariant/002
|
||||
|
||||
## Mental model: render → slot/key → effect → runtime
|
||||
|
||||
- **Render:** hemplate renders Rust view structs into checked HTML. App code
|
||||
normally reaches rendering through generated `ui::page(...)` helpers at a
|
||||
server page boundary, with explicit `SafeHtml` only for already-rendered
|
||||
fragments, not raw HTML construction. req: html_safety/002 req: html_safety/004 req: view/001
|
||||
- **Slot/key:** a generated slot names the target, and a generated keyed slot
|
||||
also carries the stable row key. A list target inside `h-for` must have a
|
||||
stable `h-key`, so row updates are addressable without CSS selectors. req: list/001
|
||||
- **Effect:** handlers return typed commands that become a checked effect
|
||||
response. Tuple composition is the normal fixed batch syntax; arrays and
|
||||
`Vec<T: IntoEffect>` cover fixed or dynamic repeated partial updates.
|
||||
- **Reuse:** the hemx answer to framework components is reusable hemplate
|
||||
partials plus generated helpers, app-owned state, `IntoEffect` composition,
|
||||
and explicit leaf islands when browser-owned behavior is necessary. See
|
||||
`docs/recipes/reusable-partials.md`.
|
||||
- **Runtime:** the browser checks the build fingerprint, resolves targets within
|
||||
the current `data-hemx-root`, and applies compatible batches. Mismatched
|
||||
server/runtime builds fail closed instead of silently mutating the wrong DOM.
|
||||
req: abi/002 req: check/004 req: failure/005
|
||||
|
||||
## Forms and errors
|
||||
|
||||
Forms remain HTML forms. hemx checks the generated form contract against a
|
||||
user-authored Rust form type, so domain newtypes such as `Email`, `TodoId`, and
|
||||
`Title` parse through ordinary Rust traits rather than generated DTOs. req: form/001
|
||||
|
||||
Use validation effects for expected user mistakes, and `Result<impl IntoEffect,
|
||||
E>` for fallible domain, database, or infrastructure work. Integration crates map
|
||||
`E` to generated UI effects, redirects, events, or HTTP responses; the canonical
|
||||
app code still uses the same handler shape for plain and fallible handlers. See
|
||||
`docs/diagnostics.md` for common compile/build/runtime mistakes and fixes.
|
||||
req: failure/004 req: derive_handler/004
|
||||
|
||||
## Pages, push, CSS, and islands
|
||||
|
||||
- Page navigation is a specialized generated page/slot effect around real
|
||||
anchors and ordinary HTTP routes. hemx does not own routing. req: modes/002 req: axum_integration/002
|
||||
- Server push streams send checked effect responses over framework-managed
|
||||
transports such as SSE; auth and connection policy stay in the server
|
||||
integration. req: wire/004 req: push/002
|
||||
- Appearance is plain CSS. Generated class tokens can make dynamic classes
|
||||
checked, but hemx does not introduce a styling runtime. req: style/001 req: style/002 req: style/003 req: style/004 req: style/005 req: style/006
|
||||
- Custom JavaScript belongs at explicit opaque leaf boundaries: charts, maps,
|
||||
editors, Web Components, or similar widgets. Islands communicate through
|
||||
generated handles/events and do not create a second UI model. req: canonical_authoring/007 req: canonical_authoring/017 req: interop/003
|
||||
|
||||
## Production boundary
|
||||
|
||||
hemx is not a SaaS platform. Production concerns stay in normal Rust/web crates
|
||||
and integrate at explicit boundaries. req: laws/002 req: auth/001
|
||||
|
||||
- **Persistence:** use SQLx or another storage adapter in your application
|
||||
state/handlers. hemx should see ordinary domain values and generated UI
|
||||
commands, not own the database layer. See `docs/recipes/sqlx-persistence.md`.
|
||||
- **Auth/session:** use Axum/Tower extractors and middleware. Handlers may accept
|
||||
typed auth/session context and return ordinary HTTP failures or generated UI
|
||||
failures. See `docs/recipes/auth-session-csrf.md`. req: auth/002
|
||||
- **CSRF:** keep CSRF policy in middleware/extractors with hidden form fields,
|
||||
cookies, and normal SameSite/browser semantics. hemx preserves submitted form
|
||||
fields and credentials semantics. See `docs/recipes/auth-session-csrf.md`. req: auth/004 req: auth/005
|
||||
- **Observability, feature flags, killswitches, deploy:** use explicit platform
|
||||
integrations around handlers, routes, runtime assets, and mobile shells. Core
|
||||
hemx must not vendor providers or add framework-specific magic. See
|
||||
`docs/recipes/observability-flags.md`, `docs/recipes/deploy-versioning.md`,
|
||||
and `docs/recipes/mobile-release.md`. req: examples/011
|
||||
- **Mobile starter:** create the phone-first path with `cargo run -p
|
||||
hemx-xtask -- app new --mobile PATH`. The starter carries a real app flow,
|
||||
typed host capabilities, command/event/projection recovery truth, and
|
||||
inspectable mobile release-kit commands without adding a native UI framework.
|
||||
Use it for Rust-owned hypermedia apps; use explicit native shells/islands for
|
||||
heavy native UI, games, camera-heavy flows, deep OS integration, or complex
|
||||
offline sync. req: ceremony/006 req: host/002
|
||||
- **PWA/offline/sync:** optional adapters may reuse generated targets/effects,
|
||||
but core hemx must not gain a mandatory client state graph or local app
|
||||
runtime. Local truth is commands/events/projections, not stored DOM patches or
|
||||
stored `EffectBatch` payloads. See `docs/recipes/pwa-offline.md` and
|
||||
`docs/recipes/local-command-log.md`. req: canonical_authoring/008 req: canonical_authoring/018 req: canonical_authoring/019 req: local/001 req: local/002
|
||||
- **Host capabilities:** browser, PWA, WebView, and native-shell capabilities use
|
||||
`hemx-host` manifests/calls/events. Adapters return host facts to app code;
|
||||
UI still changes through normal hemx effects. See
|
||||
`docs/recipes/host-capabilities.md`. req: host/001 req: host/002 req: host/005
|
||||
|
||||
## Escape hatches
|
||||
|
||||
Advanced APIs are named and isolated. Raw effects, low-level ids, manual
|
||||
registries, raw HTML/render/target construction, runtime hooks, SSE internals,
|
||||
and island internals are for integration crates, tests, migrations, or explicit
|
||||
leaf boundaries. They should not appear in beginner examples or ordinary handler
|
||||
docs. req: public_api/002 req: public_api/005
|
||||
|
||||
## Versioning and deploy compatibility
|
||||
|
||||
The generated API, symbols, effect wire schema, and JavaScript runtime carry
|
||||
schema/ABI versions. Deploy a matching server, generated output, and runtime
|
||||
asset together. Build fingerprints are derived from the generated surface and ABI
|
||||
parts; the runtime refuses incompatible effect responses and integrations should
|
||||
fall back to a full page reload when possible. See `docs/recipes/deploy-versioning.md`. req: abi/001 req: abi/002 req: failure/005
|
||||
|
||||
Before a v1 release, the semver policy and upgrade notes should explicitly state
|
||||
which surfaces are stable: beginner generated helpers and handler shapes; the
|
||||
wire/runtime ABI; and advanced escape hatches that may remain integration-level.
|
||||
See `docs/versioning.md`.
|
||||
|
||||
## Examples
|
||||
|
||||
- `examples/v0`: canonical beginner path covering counter, typed todo CRUD,
|
||||
form wizard, auth action, page swaps, SSE notifications, and keyed list
|
||||
updates. Create the generic starter with `cargo run -p hemx-xtask -- app new
|
||||
PATH`; it includes a page, form, keyed row partial, notice slot, handlers,
|
||||
tests, and generated append/replace/remove/dynamic-batch updates. Start here.
|
||||
req: ceremony/005
|
||||
- `examples/html_examples`: copy-paste HTML pattern gallery for htmx-style
|
||||
CRUD/form/search/load UX patterns. It proves the hemx idiom for click-to-edit,
|
||||
edit row, delete row, inline validation, click-to-load, and active search with
|
||||
`.heml`, generated resources, server-owned Rust state, and tiny runtime
|
||||
behavior. req: htmx_equivalents/001 req: htmx_equivalents/005 req: examples/001
|
||||
- `examples/saas`: compile-tested v1 tutorial app covering auth/session,
|
||||
CSRF-safe mutation, local persistence, generated swaps, page/push shape, plain
|
||||
CSS, and one explicit island without provider-heavy platform scope. Read the
|
||||
walkthrough in `docs/tutorial-saas.md`; the SQLx persistence recipe in
|
||||
`docs/recipes/sqlx-persistence.md` shows the provider boundary without moving
|
||||
SQL into core.
|
||||
- `examples/workout`: phone-first local-first product exemplar. Create a starter
|
||||
with `cargo run -p hemx-xtask -- workout new PATH`; run the exemplar with
|
||||
`cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`.
|
||||
It keeps workout truth as commands/events/projections and routes export
|
||||
through the host capability boundary; `examples/workout/README.md` documents
|
||||
the canonical dev, test, production build, mobile release, verification, and
|
||||
failure-mode paths. req: examples/001 req: examples/006 req: local/001 req: host/005
|
||||
- `examples/kanban`: advanced / north-star milestone boundary sketch. It may
|
||||
expose manual registry or render escape hatches while exploring product limits.
|
||||
- `examples/techdemo`: advanced integration demo with a leaf island and broader
|
||||
product interactions.
|
||||
|
||||
## Local checks
|
||||
|
||||
```sh
|
||||
# Fast compile/regression pass for the crate you touched.
|
||||
cargo test -p <crate>
|
||||
|
||||
# Focused real-browser smoke for the HTML pattern gallery.
|
||||
cargo run -p hemx-xtask -- html-examples-smoke
|
||||
|
||||
# Full local authority check; keep this green before shipping broad slices.
|
||||
cargo run -p hemx-xtask -- test
|
||||
|
||||
cargo check --workspace
|
||||
redgate health --strict
|
||||
```rust,ignore
|
||||
#[hemx::handler]
|
||||
async fn add_todo(form: NewTodo) -> impl IntoEffect {
|
||||
ui::todos().append(TodoRow::from(form))
|
||||
}
|
||||
```
|
||||
|
||||
Use `cargo run -p hemx-xtask -- test` for the full local verification path so
|
||||
jobs stay capped for local CPU and memory. The focused browser tier is
|
||||
`cargo run -p hemx-xtask -- html-examples-smoke`; it owns dynamic html_examples
|
||||
browser behavior and should complete in about 30 seconds locally. The full tier
|
||||
should complete within a 10 minute local timeout; if it grows beyond that, split
|
||||
it into deterministic repo-owned shards that together cover the same behavior,
|
||||
with `cargo run -p hemx-xtask -- test` remaining the full authority wrapper.
|
||||
req: test/004 req: test/006 req: test/015 req: test/016
|
||||
```html
|
||||
<form data-hemx-form="new_todo">
|
||||
<input name="title" required>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
<ul data-hemx-slot="todos"></ul>
|
||||
```
|
||||
|
||||
## Design boundary
|
||||
|
||||
Hemx owns checked UI effects and their browser runtime. It does not own routing,
|
||||
databases, authentication, CSS, or application state. Those remain ordinary
|
||||
Rust and web concerns. Browser-specific behavior belongs in explicit leaf islands
|
||||
rather than a second application model.
|
||||
|
||||
The normal application path is:
|
||||
|
||||
- `hemx` for handler and effect APIs;
|
||||
- `hemx-build` for generated resources;
|
||||
- `hemx-axum` for Axum integration;
|
||||
- `hemx-js` for the browser effect runtime.
|
||||
|
||||
Server-side Wasm uses the same core crates and canonical effect batches; it does
|
||||
not require a separate Hemx Wasm runtime package.
|
||||
|
||||
## Workspace
|
||||
|
||||
| Package | Purpose |
|
||||
| --- | --- |
|
||||
| `hemx` | Application-facing facade and handler macro export |
|
||||
| `hemx-core` | Effect types, protocol values, validation, and runtime primitives |
|
||||
| `hemx-derive` | Procedural macros |
|
||||
| `hemx-build` | Build-time template analysis and generated resources |
|
||||
| `hemx-axum` | Axum routes, responses, assets, and server push |
|
||||
| `hemx-js` | Browser runtime source |
|
||||
| `hemx-test` | Test support for applications |
|
||||
|
||||
## Agent skill
|
||||
|
||||
The optional [`idiomatic-hemx`](https://github.com/tmk241/hemx-skills) skill
|
||||
helps coding agents apply Hemx's generated-resource and server-owned effect
|
||||
model:
|
||||
|
||||
```console
|
||||
npx skills@latest add tmk241/hemx-skills --skill idiomatic-hemx
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```console
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
cargo test --workspace --all-targets --all-features
|
||||
cargo deny check licenses sources
|
||||
```
|
||||
|
||||
## Related projects and acknowledgements
|
||||
|
||||
[htmx](https://htmx.org/) helped popularize the HTML-over-the-wire,
|
||||
hypermedia-driven approach that inspired Hemx. Hemx is an independent
|
||||
implementation and does not bundle htmx.
|
||||
|
||||
Hemx builds on [Hemplate](https://github.com/tmk241/hemplate),
|
||||
[Tokio](https://github.com/tokio-rs/tokio),
|
||||
[Axum](https://github.com/tokio-rs/axum), and the Rust procedural-macro
|
||||
ecosystem. Thank you to their maintainers and contributors.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Reference in New Issue
Block a user