feat(wasm): execute client handlers without requests
req: client_local/001\nreq: client_local/004\nreq: client_local/005\nreq: client_local/009\nreq: client_local/010\nreq: client_local/014\nreq: v1_release/001
This commit is contained in:
Generated
+35
-15
@@ -486,6 +486,7 @@ dependencies = [
|
|||||||
"hemplate",
|
"hemplate",
|
||||||
"hemx-core",
|
"hemx-core",
|
||||||
"hemx-derive",
|
"hemx-derive",
|
||||||
|
"hemx-wasm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -510,6 +511,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hemx-client-local-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"hemplate",
|
||||||
|
"hemx",
|
||||||
|
"hemx-build",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hemx-core"
|
name = "hemx-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -634,6 +644,16 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hemx-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"hemx-core",
|
||||||
|
"thirtyfour 0.36.1",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hemx-workout-example"
|
name = "hemx-workout-example"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -900,13 +920,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.98"
|
version = "0.3.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
|
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"once_cell",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1773,6 +1792,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"stringmatch",
|
"stringmatch",
|
||||||
|
"thirtyfour-macros",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -2090,9 +2110,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.121"
|
version = "0.2.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
|
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -2103,9 +2123,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.71"
|
version = "0.4.75"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
|
checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -2113,9 +2133,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.121"
|
version = "0.2.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
|
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -2123,9 +2143,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.121"
|
version = "0.2.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
|
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2136,18 +2156,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.121"
|
version = "0.2.125"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
|
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.98"
|
version = "0.3.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
|
checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["hemx", "hemx-core", "hemx-host", "hemx-derive", "hemx-js", "hemx-axum", "hemx-build", "hemx-test", "hemx-lsp", "hemx-xtask", "examples/v0", "examples/html_examples", "examples/kanban", "examples/techdemo", "examples/saas", "examples/workout"]
|
members = ["hemx", "hemx-core", "hemx-host", "hemx-derive", "hemx-js", "hemx-axum", "hemx-build", "hemx-test", "hemx-wasm", "hemx-lsp", "hemx-xtask", "examples/v0", "examples/html_examples", "examples/kanban", "examples/client_local", "examples/techdemo", "examples/saas", "examples/workout"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ encryption, retention, backup, and deployment policy remain host concerns.
|
|||||||
## Slice 1 — one real client-local handler
|
## Slice 1 — one real client-local handler
|
||||||
|
|
||||||
- [ ] **User value:** a Rust author marks one high-frequency handler local and gets immediate browser behavior without app-authored JavaScript or a request.
|
- [ ] **User value:** a Rust author marks one high-frequency handler local and gets immediate browser behavior without app-authored JavaScript or a request.
|
||||||
- **State:** Ready.
|
- **State:** In progress. The first zero-parameter handler now compiles to real WASM, is registered by the runtime, applies the ordinary generated-target `EffectBatch`, and is browser-proven with zero interaction request. Typed events/state and fallback/error recovery remain.
|
||||||
- **Build:** add the smallest optional `hemx-wasm` boundary for `#[hemx::handler(client)]`; export only opted-in handlers; generate typed event/state ABI glue; run one existing generated-target interaction through the ordinary `EffectBatch` interpreter; preserve an explicit native/server fallback.
|
- **Build:** add the smallest optional `hemx-wasm` boundary for `#[hemx::handler(client)]`; export only opted-in handlers; generate typed event/state ABI glue; run one existing generated-target interaction through the ordinary `EffectBatch` interpreter; preserve an explicit native/server fallback.
|
||||||
- **Refusals:** no VDOM, component lifecycle, global store, sync queue, second effect protocol, or generic WASM framework.
|
- **Refusals:** no VDOM, component lifecycle, global store, sync queue, second effect protocol, or generic WASM framework.
|
||||||
- **Requirements:** `client_local/001-010`, `security/001`, `security/005-006`, `performance/003`, `v1_release/001`.
|
- **Requirements:** `client_local/001-010`, `security/001`, `security/005-006`, `performance/003`, `v1_release/001`.
|
||||||
- **Proof:** real WASM/browser test visibly updates a generated target, network capture remains empty, invalid event/state restores pending UI with a diagnostic, server handlers are unchanged, and formatting/workspace tests/strict all-target Clippy/wasm-target checks pass.
|
- **Proof:** `cargo test -p hemx-wasm --test browser client_handler_applies_effect_batch_without_network -- --exact` visibly updates a generated target through real WASM and keeps the resource count unchanged. Slice completion additionally requires invalid event/state recovery, unchanged server handlers, and formatting/workspace tests/strict all-target Clippy/wasm-target checks.
|
||||||
|
|
||||||
## Slice 2 — direct manipulation that survives interruption
|
## Slice 2 — direct manipulation that survives interruption
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "hemx-client-local-example"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hemx = { path = "../../hemx", features = ["client"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
hemplate = { path = "../../../hemplate/hemplate" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
hemx-build = { path = "../../hemx-build" }
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fn main() {
|
||||||
|
hemx_build::app()
|
||||||
|
.run()
|
||||||
|
.expect("compile client-local template");
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#[hemx::surface]
|
||||||
|
pub mod ui {}
|
||||||
|
|
||||||
|
#[hemx::handler(client)]
|
||||||
|
pub fn increment() -> impl hemx::IntoEffect {
|
||||||
|
ui::client_local::counter_panel.text("updated by Rust/WASM")
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<main data-hemx-root="client_local">
|
||||||
|
<section data-hemx-slot="counter_panel">idle</section>
|
||||||
|
<button type="button" data-hemx-handle="increment" data-hemx-client="increment">Increment locally</button>
|
||||||
|
</main>
|
||||||
+29
-1
@@ -649,6 +649,7 @@ impl Resources {
|
|||||||
out.push_str(&format!("{inner} pub fn text(self, value: impl ::std::string::ToString) -> ::hemx::advanced::Effect {{ self.slot.text(value) }}\n"));
|
out.push_str(&format!("{inner} pub fn text(self, value: impl ::std::string::ToString) -> ::hemx::advanced::Effect {{ self.slot.text(value) }}\n"));
|
||||||
out.push_str(&format!("{inner} pub fn set(self, value: impl ::std::string::ToString) -> ::hemx::advanced::Effect {{ self.slot.text(value) }}\n"));
|
out.push_str(&format!("{inner} pub fn set(self, value: impl ::std::string::ToString) -> ::hemx::advanced::Effect {{ self.slot.text(value) }}\n"));
|
||||||
out.push_str(&format!("{inner}}}\n"));
|
out.push_str(&format!("{inner}}}\n"));
|
||||||
|
out.push_str(&format!("{inner}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{inner}impl<T> SlotTarget<T, ()> {{\n"));
|
out.push_str(&format!("{inner}impl<T> SlotTarget<T, ()> {{\n"));
|
||||||
out.push_str(&format!("{inner} pub fn put(self, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect {{ super::put(self.slot, view) }}\n"));
|
out.push_str(&format!("{inner} pub fn put(self, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect {{ super::put(self.slot, view) }}\n"));
|
||||||
out.push_str(&format!("{inner} pub fn replace(self, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect {{ super::put(self.slot, view) }}\n"));
|
out.push_str(&format!("{inner} pub fn replace(self, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect {{ super::put(self.slot, view) }}\n"));
|
||||||
@@ -660,6 +661,7 @@ impl Resources {
|
|||||||
.filter(|res| components.contains(&res.ident) && Some(res.ident.as_str()) != component)
|
.filter(|res| components.contains(&res.ident) && Some(res.ident.as_str()) != component)
|
||||||
{
|
{
|
||||||
let child = &res.ident;
|
let child = &res.ident;
|
||||||
|
out.push_str(&format!("{inner}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"{inner}impl<T> SlotTarget<T, {child_prefix}{child}::Component> {{\n"
|
"{inner}impl<T> SlotTarget<T, {child_prefix}{child}::Component> {{\n"
|
||||||
));
|
));
|
||||||
@@ -681,6 +683,7 @@ impl Resources {
|
|||||||
out.push_str(&format!("{inner}{{\n"));
|
out.push_str(&format!("{inner}{{\n"));
|
||||||
out.push_str(&format!("{inner} const fn new(slot: ::hemx::advanced::KeyedSlot<K, T>) -> Self {{ Self {{ slot, _marker: ::std::marker::PhantomData }} }}\n"));
|
out.push_str(&format!("{inner} const fn new(slot: ::hemx::advanced::KeyedSlot<K, T>) -> Self {{ Self {{ slot, _marker: ::std::marker::PhantomData }} }}\n"));
|
||||||
out.push_str(&format!("{inner}}}\n"));
|
out.push_str(&format!("{inner}}}\n"));
|
||||||
|
out.push_str(&format!("{inner}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"{inner}impl<T> KeyedSlotTarget<::std::string::String, T, ()> {{\n"
|
"{inner}impl<T> KeyedSlotTarget<::std::string::String, T, ()> {{\n"
|
||||||
));
|
));
|
||||||
@@ -700,6 +703,7 @@ impl Resources {
|
|||||||
.filter(|res| components.contains(&res.ident) && Some(res.ident.as_str()) != component)
|
.filter(|res| components.contains(&res.ident) && Some(res.ident.as_str()) != component)
|
||||||
{
|
{
|
||||||
let child = &res.ident;
|
let child = &res.ident;
|
||||||
|
out.push_str(&format!("{inner}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"{inner}impl<T> KeyedSlotTarget<::std::string::String, T, {child_prefix}{child}::Component> {{\n"
|
"{inner}impl<T> KeyedSlotTarget<::std::string::String, T, {child_prefix}{child}::Component> {{\n"
|
||||||
));
|
));
|
||||||
@@ -948,6 +952,7 @@ impl Resources {
|
|||||||
"{pad} ::hemx::__private::html_trusted(lower(html))\n"
|
"{pad} ::hemx::__private::html_trusted(lower(html))\n"
|
||||||
));
|
));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"{pad}pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{\n"
|
"{pad}pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{\n"
|
||||||
@@ -961,14 +966,17 @@ impl Resources {
|
|||||||
));
|
));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"{pad}pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n"
|
"{pad}#[cfg(not(target_arch = \"wasm32\"))]\n{pad}pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n"
|
||||||
));
|
));
|
||||||
|
out.push_str(&format!("{pad}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
||||||
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n"));
|
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
||||||
out.push_str(&format!("{pad}pub fn put<T>(slot: ::hemx::advanced::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect {{\n"));
|
out.push_str(&format!("{pad}pub fn put<T>(slot: ::hemx::advanced::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect {{\n"));
|
||||||
out.push_str(&format!("{pad} slot.html(render(view))\n"));
|
out.push_str(&format!("{pad} slot.html(render(view))\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
||||||
out.push_str(&format!("{pad}pub fn append<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect\n"));
|
out.push_str(&format!("{pad}pub fn append<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect\n"));
|
||||||
out.push_str(&format!("{pad}where\n"));
|
out.push_str(&format!("{pad}where\n"));
|
||||||
@@ -976,6 +984,7 @@ impl Resources {
|
|||||||
out.push_str(&format!("{pad}{{\n"));
|
out.push_str(&format!("{pad}{{\n"));
|
||||||
out.push_str(&format!("{pad} slot.append_html(key, render(view))\n"));
|
out.push_str(&format!("{pad} slot.append_html(key, render(view))\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
||||||
out.push_str(&format!("{pad}pub fn prepend<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect\n"));
|
out.push_str(&format!("{pad}pub fn prepend<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect\n"));
|
||||||
out.push_str(&format!("{pad}where\n"));
|
out.push_str(&format!("{pad}where\n"));
|
||||||
@@ -983,6 +992,7 @@ impl Resources {
|
|||||||
out.push_str(&format!("{pad}{{\n"));
|
out.push_str(&format!("{pad}{{\n"));
|
||||||
out.push_str(&format!("{pad} slot.prepend_html(key, render(view))\n"));
|
out.push_str(&format!("{pad} slot.prepend_html(key, render(view))\n"));
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
out.push_str(&format!("{pad}}}\n\n"));
|
||||||
|
out.push_str(&format!("{pad}#[cfg(not(target_arch = \"wasm32\"))]\n"));
|
||||||
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
out.push_str(&format!("{pad}#[doc(hidden)]\n"));
|
||||||
out.push_str(&format!("{pad}pub fn replace<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect\n"));
|
out.push_str(&format!("{pad}pub fn replace<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect\n"));
|
||||||
out.push_str(&format!("{pad}where\n"));
|
out.push_str(&format!("{pad}where\n"));
|
||||||
@@ -1597,6 +1607,8 @@ fn known_hemx_attr(name: &str) -> bool {
|
|||||||
| "data-hemx-atom"
|
| "data-hemx-atom"
|
||||||
| "data-hemx-key"
|
| "data-hemx-key"
|
||||||
| "data-hemx-on"
|
| "data-hemx-on"
|
||||||
|
| "data-hemx-client"
|
||||||
|
| "data-hemx-client-event"
|
||||||
| "data-hemx-pending-class"
|
| "data-hemx-pending-class"
|
||||||
| "data-hemx-indicator"
|
| "data-hemx-indicator"
|
||||||
| "data-hemx-confirm"
|
| "data-hemx-confirm"
|
||||||
@@ -1640,6 +1652,22 @@ fn reject_invalid_hemx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
|
|||||||
"expected `push`, `replace`, or empty for the default push behavior",
|
"expected `push`, `replace`, or empty for the default push behavior",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
"data-hemx-client" if value.trim().is_empty() => {
|
||||||
|
return Err(invalid_hemx_value(
|
||||||
|
path,
|
||||||
|
&attr.name,
|
||||||
|
value,
|
||||||
|
"expected a non-empty client handler name",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"data-hemx-client-event" if !valid_event_list(value) => {
|
||||||
|
return Err(invalid_hemx_value(
|
||||||
|
path,
|
||||||
|
&attr.name,
|
||||||
|
value,
|
||||||
|
"expected a runtime-supported event",
|
||||||
|
));
|
||||||
|
}
|
||||||
"data-hemx-on" if !valid_event_list(value) => {
|
"data-hemx-on" if !valid_event_list(value) => {
|
||||||
return Err(invalid_hemx_value(
|
return Err(invalid_hemx_value(
|
||||||
path,
|
path,
|
||||||
|
|||||||
+53
-2
@@ -10,7 +10,20 @@ use syn::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let placement = attr.to_string();
|
||||||
|
let is_client = match placement.as_str() {
|
||||||
|
"" => false,
|
||||||
|
"client" => true,
|
||||||
|
_ => {
|
||||||
|
return syn::Error::new(
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
"unsupported hemx handler placement; expected #[hemx::handler] or #[hemx::handler(client)]",
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
};
|
||||||
let function = parse_macro_input!(item as ItemFn);
|
let function = parse_macro_input!(item as ItemFn);
|
||||||
let name = function.sig.ident.to_string();
|
let name = function.sig.ident.to_string();
|
||||||
|
|
||||||
@@ -73,7 +86,45 @@ pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
quote!(#function).into()
|
if !is_client {
|
||||||
|
return quote!(#function).into();
|
||||||
|
}
|
||||||
|
if !function.sig.inputs.is_empty()
|
||||||
|
|| function.sig.asyncness.is_some()
|
||||||
|
|| function.sig.unsafety.is_some()
|
||||||
|
|| function.sig.constness.is_some()
|
||||||
|
|| !function.sig.generics.params.is_empty()
|
||||||
|
{
|
||||||
|
let message = format!(
|
||||||
|
"client-local hemx handler `{name}` must be a safe, synchronous, non-generic function with no parameters"
|
||||||
|
);
|
||||||
|
return quote!(
|
||||||
|
#function
|
||||||
|
compile_error!(#message);
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let function_name = &function.sig.ident;
|
||||||
|
let export_name = format_ident!("__hemx_client_{function_name}");
|
||||||
|
let export_module = format_ident!("__hemx_client_export_{function_name}");
|
||||||
|
quote!(
|
||||||
|
#function
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod #export_module {
|
||||||
|
use ::hemx::wasm as wasm_bindgen;
|
||||||
|
|
||||||
|
#[::hemx::wasm::wasm_bindgen(js_name = #export_name)]
|
||||||
|
pub fn invoke() -> ::std::vec::Vec<u8> {
|
||||||
|
::hemx::wasm::encode_handler_effect(
|
||||||
|
super::#function_name(),
|
||||||
|
crate::ui::BUILD_FINGERPRINT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
|
|||||||
Vendored
+1
@@ -54,6 +54,7 @@ export interface HemxRuntime {
|
|||||||
decodeBatch(buffer: ArrayBuffer): EffectBatch;
|
decodeBatch(buffer: ArrayBuffer): EffectBatch;
|
||||||
atomValue(root: Element | ParentNode | null | undefined, id: number): Uint8Array | undefined;
|
atomValue(root: Element | ParentNode | null | undefined, id: number): Uint8Array | undefined;
|
||||||
decodeAtomState(encoded: string): AtomSnapshot[];
|
decodeAtomState(encoded: string): AtomSnapshot[];
|
||||||
|
registerClientHandler(name: string, handler: () => Uint8Array | Promise<Uint8Array>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
+29
-2
@@ -18,6 +18,7 @@
|
|||||||
const sseSources = new WeakMap();
|
const sseSources = new WeakMap();
|
||||||
const atomStores = new WeakMap();
|
const atomStores = new WeakMap();
|
||||||
const dragKeys = new WeakMap();
|
const dragKeys = new WeakMap();
|
||||||
|
const clientHandlers = new Map();
|
||||||
|
|
||||||
function roots() {
|
function roots() {
|
||||||
const found = [];
|
const found = [];
|
||||||
@@ -237,6 +238,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runClient(el) {
|
||||||
|
const name = el.getAttribute("data-hemx-client");
|
||||||
|
const handler = clientHandlers.get(name);
|
||||||
|
if (!handler) throw new Error(`unknown client-local hemx handler: ${name}`);
|
||||||
|
const wire = await handler();
|
||||||
|
if (!(wire instanceof Uint8Array)) throw new Error(`client-local hemx handler ${name} returned an invalid effect batch`);
|
||||||
|
applyBatch(wire, rootOf(el));
|
||||||
|
}
|
||||||
|
|
||||||
async function send(el, eventName, source = el) {
|
async function send(el, eventName, source = el) {
|
||||||
if (el.getAttribute("data-hemx-confirm") && !confirm(el.getAttribute("data-hemx-confirm"))) return;
|
if (el.getAttribute("data-hemx-confirm") && !confirm(el.getAttribute("data-hemx-confirm"))) return;
|
||||||
const { form, data, multipart } = formDataFor(el, eventName, source);
|
const { form, data, multipart } = formDataFor(el, eventName, source);
|
||||||
@@ -671,7 +681,11 @@
|
|||||||
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID));
|
const direct = closestInRoot(event.target, root, (el) => el.hasAttribute(HID));
|
||||||
if (direct && defaultEvent(direct) === "click") {
|
if (direct && defaultEvent(direct) === "click") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
schedule(direct, name);
|
if (direct.hasAttribute("data-hemx-client")) {
|
||||||
|
runClient(direct).catch((error) => emit(root, "hemx:client-error", String(error)));
|
||||||
|
} else {
|
||||||
|
schedule(direct, name);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,7 +949,20 @@
|
|||||||
if (root) navigateUrl(location.href, root, "none").catch((error) => emit(root, "hemx:error", String(error)));
|
if (root) navigateUrl(location.href, root, "none").catch((error) => emit(root, "hemx:error", String(error)));
|
||||||
});
|
});
|
||||||
|
|
||||||
window.hemx = Object.freeze({ runtimeAbiVersion, roots, rootOf, applyHtml, applyBatch, decodeBatch, atomValue, decodeAtomState });
|
window.hemx = Object.freeze({
|
||||||
|
runtimeAbiVersion,
|
||||||
|
roots,
|
||||||
|
rootOf,
|
||||||
|
applyHtml,
|
||||||
|
applyBatch,
|
||||||
|
decodeBatch,
|
||||||
|
atomValue,
|
||||||
|
decodeAtomState,
|
||||||
|
registerClientHandler(name, handler) {
|
||||||
|
if (!name || typeof handler !== "function") throw new Error("client handler registration requires a name and function");
|
||||||
|
clientHandlers.set(name, handler);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
|
||||||
else start();
|
else start();
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "hemx-wasm"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hemx-core = { path = "../hemx-core" }
|
||||||
|
wasm-bindgen = "=0.2.125"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
thirtyfour = "0.36"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//! Opt-in browser boundary for client-local hemx handlers.
|
||||||
|
//!
|
||||||
|
//! Application code reaches this crate through the `hemx` `client` feature and
|
||||||
|
//! `#[hemx::handler(client)]`; server-first applications do not depend on it.
|
||||||
|
|
||||||
|
use hemx_core::{BuildFingerprint, IntoEffect};
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use wasm_bindgen::*;
|
||||||
|
|
||||||
|
/// Encodes a client handler result with the ordinary hemx effect wire format.
|
||||||
|
///
|
||||||
|
/// Keeping this conversion here gives generated WASM exports one ABI boundary
|
||||||
|
/// instead of teaching the proc macro a second effect protocol.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn encode_handler_effect(effect: impl IntoEffect, fingerprint: BuildFingerprint) -> Vec<u8> {
|
||||||
|
effect.into_batch(fingerprint).to_wire()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::encode_handler_effect;
|
||||||
|
use hemx_core::{BuildFingerprint, EffectBatch, Slot};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_handler_uses_the_ordinary_effect_wire_format() {
|
||||||
|
let fingerprint = BuildFingerprint(17);
|
||||||
|
let effect = Slot::<()>::new(4).text("local");
|
||||||
|
let wire = encode_handler_effect(effect.clone(), fingerprint);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
EffectBatch::from_wire(&wire).expect("decode client effect"),
|
||||||
|
EffectBatch {
|
||||||
|
abi_version: hemx_core::EFFECT_BATCH_ABI_VERSION,
|
||||||
|
fingerprint,
|
||||||
|
ops: vec![effect],
|
||||||
|
}
|
||||||
|
); // req: client_local/005 req: client_local/009
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::{TcpListener, TcpStream};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use thirtyfour::prelude::*;
|
||||||
|
|
||||||
|
const WEBDRIVER_ADDR: &str = "127.0.0.1:4451";
|
||||||
|
const STARTUP_TIMEOUT: Duration = Duration::from_secs(12);
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn client_handler_applies_effect_batch_without_network() -> WebDriverResult<()> {
|
||||||
|
// req: client_local/005 req: client_local/009 req: client_local/010
|
||||||
|
// test: client_local/014
|
||||||
|
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.expect("workspace root")
|
||||||
|
.to_owned();
|
||||||
|
build_browser_artifact(&workspace);
|
||||||
|
let package = workspace.join("target/client-local-bindgen");
|
||||||
|
let runtime = workspace.join("hemx-js/runtime/hemx.js");
|
||||||
|
let server = StaticServer::start(package, runtime);
|
||||||
|
|
||||||
|
let mut webdriver = Command::new("geckodriver");
|
||||||
|
webdriver.arg("--port").arg("4451");
|
||||||
|
let _webdriver = ProcessGuard::start(webdriver, WEBDRIVER_ADDR);
|
||||||
|
|
||||||
|
let mut caps = DesiredCapabilities::firefox();
|
||||||
|
caps.set_headless()?;
|
||||||
|
let driver = WebDriver::new(&format!("http://{WEBDRIVER_ADDR}"), caps).await?;
|
||||||
|
let result = async {
|
||||||
|
driver.goto(&server.url()).await?;
|
||||||
|
wait_until(&driver, "return window.__clientReady === true").await?;
|
||||||
|
let network_before = resource_count(&driver).await?;
|
||||||
|
|
||||||
|
driver
|
||||||
|
.find(By::Css("[data-hemx-client='increment']"))
|
||||||
|
.await?
|
||||||
|
.click()
|
||||||
|
.await?;
|
||||||
|
wait_for_text(
|
||||||
|
&driver,
|
||||||
|
"[data-hemx-slot='counter_panel']",
|
||||||
|
"updated by Rust/WASM",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resource_count(&driver).await?,
|
||||||
|
network_before,
|
||||||
|
"client handler made a network request"
|
||||||
|
);
|
||||||
|
Ok::<(), WebDriverError>(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
let quit = driver.quit().await;
|
||||||
|
result.and(quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_browser_artifact(workspace: &Path) {
|
||||||
|
let status = Command::new("cargo")
|
||||||
|
.current_dir(workspace)
|
||||||
|
.args([
|
||||||
|
"build",
|
||||||
|
"-p",
|
||||||
|
"hemx-client-local-example",
|
||||||
|
"--target",
|
||||||
|
"wasm32-unknown-unknown",
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.expect("run wasm cargo build");
|
||||||
|
assert!(status.success(), "WASM build failed");
|
||||||
|
|
||||||
|
let output = workspace.join("target/client-local-bindgen");
|
||||||
|
fs::create_dir_all(&output).expect("create wasm-bindgen output");
|
||||||
|
let status = Command::new("wasm-bindgen")
|
||||||
|
.current_dir(workspace)
|
||||||
|
.arg("--target")
|
||||||
|
.arg("web")
|
||||||
|
.arg("--out-name")
|
||||||
|
.arg("client_local")
|
||||||
|
.arg("--out-dir")
|
||||||
|
.arg(&output)
|
||||||
|
.arg(workspace.join("target/wasm32-unknown-unknown/debug/hemx_client_local_example.wasm"))
|
||||||
|
.status()
|
||||||
|
.expect("run wasm-bindgen");
|
||||||
|
assert!(status.success(), "wasm-bindgen failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StaticServer {
|
||||||
|
address: String,
|
||||||
|
stop: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticServer {
|
||||||
|
fn start(package: PathBuf, runtime: PathBuf) -> Self {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").expect("bind browser fixture");
|
||||||
|
listener.set_nonblocking(true).expect("nonblocking fixture");
|
||||||
|
let address = listener.local_addr().expect("fixture address").to_string();
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let thread_stop = Arc::clone(&stop);
|
||||||
|
thread::spawn(move || {
|
||||||
|
while !thread_stop.load(Ordering::Relaxed) {
|
||||||
|
match listener.accept() {
|
||||||
|
Ok((stream, _)) => serve(stream, &package, &runtime),
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
thread::sleep(Duration::from_millis(10))
|
||||||
|
}
|
||||||
|
Err(error) => panic!("fixture accept failed: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Self { address, stop }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> String {
|
||||||
|
format!("http://{}", self.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for StaticServer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve(mut stream: TcpStream, package: &Path, runtime: &Path) {
|
||||||
|
let mut request = [0_u8; 2048];
|
||||||
|
let length = stream.read(&mut request).unwrap_or(0);
|
||||||
|
let first = String::from_utf8_lossy(&request[..length]);
|
||||||
|
let path = first.split_whitespace().nth(1).unwrap_or("/");
|
||||||
|
let (content_type, body) = match path {
|
||||||
|
"/" => ("text/html; charset=utf-8", fixture_html().into_bytes()),
|
||||||
|
"/hemx.js" => (
|
||||||
|
"text/javascript; charset=utf-8",
|
||||||
|
fs::read(runtime).expect("read runtime"),
|
||||||
|
),
|
||||||
|
"/client_local.js" => (
|
||||||
|
"text/javascript; charset=utf-8",
|
||||||
|
fs::read(package.join("client_local.js")).expect("read bindings"),
|
||||||
|
),
|
||||||
|
"/client_local_bg.wasm" => (
|
||||||
|
"application/wasm",
|
||||||
|
fs::read(package.join("client_local_bg.wasm")).expect("read wasm"),
|
||||||
|
),
|
||||||
|
_ => ("text/plain", b"not found".to_vec()),
|
||||||
|
};
|
||||||
|
let status = if path == "/" || path == "/hemx.js" || path.starts_with("/client_local") {
|
||||||
|
"200 OK"
|
||||||
|
} else {
|
||||||
|
"404 Not Found"
|
||||||
|
};
|
||||||
|
write!(stream, "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len()).expect("write fixture headers");
|
||||||
|
stream.write_all(&body).expect("write fixture body");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_html() -> String {
|
||||||
|
r#"<!doctype html><html><body>
|
||||||
|
<main data-hemx-root="client_local">
|
||||||
|
<section data-hemx-slot="counter_panel">idle</section>
|
||||||
|
<button type="button" data-hid="1" data-hemx-client="increment">Increment locally</button>
|
||||||
|
</main>
|
||||||
|
<script src="/hemx.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import init, { __hemx_client_increment } from "/client_local.js";
|
||||||
|
await init();
|
||||||
|
const root = document.querySelector("[data-hemx-root]");
|
||||||
|
const slot = root.querySelector("[data-hemx-slot]");
|
||||||
|
const probe = __hemx_client_increment();
|
||||||
|
const batch = window.hemx.decodeBatch(probe);
|
||||||
|
root.dataset.hemxBuild = String(batch.fingerprint);
|
||||||
|
slot.dataset.sid = String(batch.ops[0].target.resource.id);
|
||||||
|
window.hemx.registerClientHandler("increment", __hemx_client_increment);
|
||||||
|
window.__clientReady = true;
|
||||||
|
</script></body></html>"#
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_until(driver: &WebDriver, script: &str) -> WebDriverResult<()> {
|
||||||
|
let deadline = Instant::now() + STARTUP_TIMEOUT;
|
||||||
|
loop {
|
||||||
|
if driver
|
||||||
|
.execute(script, Vec::new())
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.as_bool()
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
panic!("timed out waiting for browser fixture");
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_text(driver: &WebDriver, selector: &str, text: &str) -> WebDriverResult<()> {
|
||||||
|
let deadline = Instant::now() + STARTUP_TIMEOUT;
|
||||||
|
loop {
|
||||||
|
if let Ok(element) = driver.find(By::Css(selector)).await {
|
||||||
|
if element.text().await.unwrap_or_default().contains(text) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
panic!("timed out waiting for {text:?}");
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resource_count(driver: &WebDriver) -> WebDriverResult<u64> {
|
||||||
|
Ok(driver
|
||||||
|
.execute(
|
||||||
|
"return performance.getEntriesByType('resource').length",
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.as_u64()
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProcessGuard(std::process::Child);
|
||||||
|
|
||||||
|
impl ProcessGuard {
|
||||||
|
fn start(mut command: Command, address: &str) -> Self {
|
||||||
|
let child = command.spawn().expect("start geckodriver");
|
||||||
|
let deadline = Instant::now() + STARTUP_TIMEOUT;
|
||||||
|
while std::net::TcpStream::connect(address).is_err() {
|
||||||
|
assert!(Instant::now() < deadline, "timed out starting geckodriver");
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
Self(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ProcessGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.0.kill();
|
||||||
|
let _ = self.0.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -10,7 +10,14 @@ path = "src/lib.rs"
|
|||||||
name = "render"
|
name = "render"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
client = ["dep:hemx-wasm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hemplate = { path = "../../hemplate/hemplate" }
|
|
||||||
hemx-core = { path = "../hemx-core" }
|
hemx-core = { path = "../hemx-core" }
|
||||||
hemx-derive = { path = "../hemx-derive" }
|
hemx-derive = { path = "../hemx-derive" }
|
||||||
|
hemx-wasm = { path = "../hemx-wasm", optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
hemplate = { path = "../../hemplate/hemplate" }
|
||||||
|
|||||||
+19
-1
@@ -3,7 +3,9 @@
|
|||||||
//! Most application code should depend on this crate, use the proc-macros from
|
//! Most application code should depend on this crate, use the proc-macros from
|
||||||
//! here, and import generated resources through `#[hemx::surface]`.
|
//! here, and import generated resources through `#[hemx::surface]`.
|
||||||
|
|
||||||
use hemx_core::{Effect, KeyedSlot, SafeHtml, Slot};
|
use hemx_core::SafeHtml;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use hemx_core::{Effect, KeyedSlot, Slot};
|
||||||
|
|
||||||
pub use hemx_core::{
|
pub use hemx_core::{
|
||||||
navigate, push, redirect, replace, CssClass, CssClasses, Form, FormContract, FormControlKind,
|
navigate, push, redirect, replace, CssClass, CssClasses, Form, FormContract, FormControlKind,
|
||||||
@@ -19,6 +21,14 @@ pub use hemx_core::{
|
|||||||
pub use hemx_core::{Atom, ComponentRef, EventName, GeneratedTarget, Handle, ParamName};
|
pub use hemx_core::{Atom, ComponentRef, EventName, GeneratedTarget, Handle, ParamName};
|
||||||
pub use hemx_derive::{app, component, form, handler, surface};
|
pub use hemx_derive::{app, component, form, handler, surface};
|
||||||
|
|
||||||
|
/// Browser/WASM integration used by `#[hemx::handler(client)]`.
|
||||||
|
///
|
||||||
|
/// The feature is opt-in so server-first applications do not compile or ship
|
||||||
|
/// WASM dependencies. req: client_local/001 req: client_local/004
|
||||||
|
#[cfg(feature = "client")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use hemx_wasm as wasm;
|
||||||
|
|
||||||
/// Advanced/raw hemx primitives used by generated code, integrations, and tests.
|
/// Advanced/raw hemx primitives used by generated code, integrations, and tests.
|
||||||
///
|
///
|
||||||
/// Beginner-facing application code should prefer generated targets, generated
|
/// Beginner-facing application code should prefer generated targets, generated
|
||||||
@@ -26,6 +36,7 @@ pub use hemx_derive::{app, component, form, handler, surface};
|
|||||||
pub mod advanced {
|
pub mod advanced {
|
||||||
pub use hemx_core::*;
|
pub use hemx_core::*;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn render(view: &impl hemplate::Hemplate) -> crate::Html {
|
pub fn render(view: &impl hemplate::Hemplate) -> crate::Html {
|
||||||
crate::render_template(view)
|
crate::render_template(view)
|
||||||
}
|
}
|
||||||
@@ -79,6 +90,7 @@ pub mod __private {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn render_template(view: &impl hemplate::Hemplate) -> Html {
|
fn render_template(view: &impl hemplate::Hemplate) -> Html {
|
||||||
let mut html = String::with_capacity(view.size_hint());
|
let mut html = String::with_capacity(view.size_hint());
|
||||||
view.render_into(&mut html)
|
view.render_into(&mut html)
|
||||||
@@ -86,6 +98,7 @@ fn render_template(view: &impl hemplate::Hemplate) -> Html {
|
|||||||
__private::html_trusted(html)
|
__private::html_trusted(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn page(view: &impl hemplate::Hemplate) -> Html {
|
pub fn page(view: &impl hemplate::Hemplate) -> Html {
|
||||||
render_template(view)
|
render_template(view)
|
||||||
}
|
}
|
||||||
@@ -99,6 +112,7 @@ pub trait KeyedPartial {
|
|||||||
fn hemx_key(&self) -> String;
|
fn hemx_key(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn render_html(view: &impl hemplate::Hemplate) -> Html {
|
pub fn render_html(view: &impl hemplate::Hemplate) -> Html {
|
||||||
render_template(view)
|
render_template(view)
|
||||||
@@ -108,6 +122,7 @@ pub fn render_html(view: &impl hemplate::Hemplate) -> Html {
|
|||||||
///
|
///
|
||||||
/// Prefer generated target objects such as `targets::list.put(&view)` so
|
/// Prefer generated target objects such as `targets::list.put(&view)` so
|
||||||
/// generated resource lowering stays attached to the view boundary. req: dx/006
|
/// generated resource lowering stays attached to the view boundary. req: dx/006
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub trait RenderSlotExt {
|
pub trait RenderSlotExt {
|
||||||
fn render(self, view: &impl hemplate::Hemplate) -> Effect;
|
fn render(self, view: &impl hemplate::Hemplate) -> Effect;
|
||||||
@@ -120,6 +135,7 @@ pub trait RenderSlotExt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl<T> RenderSlotExt for Slot<T> {
|
impl<T> RenderSlotExt for Slot<T> {
|
||||||
fn render(self, view: &impl hemplate::Hemplate) -> Effect {
|
fn render(self, view: &impl hemplate::Hemplate) -> Effect {
|
||||||
self.html(render_template(view))
|
self.html(render_template(view))
|
||||||
@@ -130,6 +146,7 @@ impl<T> RenderSlotExt for Slot<T> {
|
|||||||
///
|
///
|
||||||
/// Prefer generated target objects such as `targets::row.append(&view)` so
|
/// Prefer generated target objects such as `targets::row.append(&view)` so
|
||||||
/// generated resource lowering stays attached to the view boundary. req: dx/006
|
/// generated resource lowering stays attached to the view boundary. req: dx/006
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub trait RenderKeyedSlotExt<K> {
|
pub trait RenderKeyedSlotExt<K> {
|
||||||
fn append(self, key: K, view: &impl hemplate::Hemplate) -> Effect;
|
fn append(self, key: K, view: &impl hemplate::Hemplate) -> Effect;
|
||||||
@@ -158,6 +175,7 @@ pub trait RenderKeyedSlotExt<K> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl<K, T> RenderKeyedSlotExt<K> for KeyedSlot<K, T>
|
impl<K, T> RenderKeyedSlotExt<K> for KeyedSlot<K, T>
|
||||||
where
|
where
|
||||||
K: ToString,
|
K: ToString,
|
||||||
|
|||||||
Reference in New Issue
Block a user