feat(build): generate client handler bootstrap
req: client_local/001\nreq: client_local/004\nreq: client_local/006\nreq: client_local/007\nreq: client_local/008\nreq: client_local/009\nreq: client_local/010\nreq: client_local/014\nreq: security/002\nreq: security/006\nreq: v1_release/001\nreq: v1_release/008
This commit is contained in:
@@ -10,4 +10,5 @@ path = "src/lib.rs"
|
||||
hemplate-core = { path = "../../hemplate/hemplate-core", features = ["surface"] }
|
||||
hemx-core = { path = "../hemx-core" }
|
||||
quote = "1"
|
||||
serde_json = "1"
|
||||
syn = { version = "2", features = ["full"] }
|
||||
|
||||
@@ -228,6 +228,10 @@ impl AppBuilder {
|
||||
resources.generated_rs(self.global_exports).as_bytes(),
|
||||
)?;
|
||||
write_if_changed(&out_dir.join("hemx.syms"), resources.syms().as_bytes())?;
|
||||
write_if_changed(
|
||||
&out_dir.join("hemx.client.js"),
|
||||
resources.client_bootstrap()?.as_bytes(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -262,6 +266,8 @@ struct Resources {
|
||||
atoms: BTreeMap<String, Resource>,
|
||||
classes: BTreeMap<String, ClassToken>,
|
||||
events: BTreeMap<String, EventToken>,
|
||||
client_handlers: BTreeSet<String>,
|
||||
client_modules: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -334,6 +340,21 @@ impl Resources {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(handler) = static_attr(&node.attrs, "data-hemx-client") {
|
||||
let Some(handler) = rust_ident(&handler) else {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
"data-hemx-client",
|
||||
&handler,
|
||||
"expected a Rust handler identifier",
|
||||
));
|
||||
};
|
||||
self.client_handlers.insert(handler);
|
||||
}
|
||||
if let Some(module) = static_attr(&node.attrs, "data-hemx-client-module") {
|
||||
self.client_modules.insert(module);
|
||||
}
|
||||
|
||||
if let Some(name) = static_attr(&node.attrs, "data-hemx-slot") {
|
||||
reject_unkeyed_loop(surface, node.scope, path, "slot", &name)?;
|
||||
let keyed = is_inside_keyed_for(surface, node.scope)
|
||||
@@ -1163,6 +1184,50 @@ fn __hemx_attr(tag: &str, attr: &str) -> Option<::std::string::String> {
|
||||
components
|
||||
}
|
||||
|
||||
fn client_bootstrap(&self) -> io::Result<String> {
|
||||
if self.client_handlers.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
let module = match self.client_modules.len() {
|
||||
1 => self.client_modules.iter().next().expect("one module"),
|
||||
0 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"client-local handlers require one data-hemx-client-module on a hemx root",
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"client-local handlers must share one data-hemx-client-module per generated application",
|
||||
));
|
||||
}
|
||||
};
|
||||
let exports = self
|
||||
.client_handlers
|
||||
.iter()
|
||||
.map(|handler| format!("__hemx_client_{handler}"))
|
||||
.collect::<Vec<_>>();
|
||||
let mut out = format!(
|
||||
"import init, {{ {} }} from {};\nawait init();\n",
|
||||
exports.join(", "),
|
||||
serde_json::to_string(module).expect("serialize client module")
|
||||
);
|
||||
for handler in &self.client_handlers {
|
||||
out.push_str(&format!(
|
||||
"window.hemx.registerClientHandler({}, __hemx_client_{});\n",
|
||||
serde_json::to_string(handler).expect("serialize client handler"),
|
||||
handler
|
||||
));
|
||||
}
|
||||
let fingerprint = hemx_core::BuildFingerprint::from_parts(&self.fingerprint_parts()).0;
|
||||
out.push_str(&format!(
|
||||
"document.querySelectorAll('[data-hemx-root]').forEach((root) => {{ root.setAttribute('data-hemx-build', '{}'); root.setAttribute('data-hemx-client-ready', ''); }});\n",
|
||||
fingerprint
|
||||
));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn syms(&self) -> String {
|
||||
let mut out = String::from("hemx-syms-v1\n");
|
||||
for res in self.slots.values() {
|
||||
@@ -1610,6 +1675,7 @@ fn known_hemx_attr(name: &str) -> bool {
|
||||
| "data-hemx-client"
|
||||
| "data-hemx-client-event"
|
||||
| "data-hemx-client-fallback"
|
||||
| "data-hemx-client-module"
|
||||
| "data-hemx-client-state-version"
|
||||
| "data-hemx-pending-class"
|
||||
| "data-hemx-indicator"
|
||||
@@ -1662,6 +1728,14 @@ fn reject_invalid_hemx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
|
||||
"expected a non-empty client handler name",
|
||||
));
|
||||
}
|
||||
"data-hemx-client-module" if !valid_client_module(value) => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a same-origin module specifier beginning with `/`, `./`, or `../`",
|
||||
));
|
||||
}
|
||||
"data-hemx-client-event" if !valid_event_list(value) => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
@@ -1748,6 +1822,13 @@ fn reject_invalid_hemx_attr_placement(
|
||||
"expected a container around descendant links/forms; use `data-hemx-nav` on anchors or `data-hemx-handle` on forms",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-client-module") && !has_attr(attrs, "data-hemx-root") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-client-module",
|
||||
"expected placement on the same element as `data-hemx-root`",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-sse") && !has_attr(attrs, "data-hemx-root") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
@@ -1776,6 +1857,14 @@ fn valid_policy(value: &str) -> bool {
|
||||
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
|
||||
}
|
||||
|
||||
fn valid_client_module(value: &str) -> bool {
|
||||
let value = value.trim();
|
||||
!value.is_empty()
|
||||
&& !value.starts_with("//")
|
||||
&& !value.contains(':')
|
||||
&& (value.starts_with('/') || value.starts_with("./") || value.starts_with("../"))
|
||||
}
|
||||
|
||||
fn valid_event_list(value: &str) -> bool {
|
||||
let mut events = event_tokens(value).peekable();
|
||||
events.peek().is_some() && events.all(valid_runtime_event)
|
||||
|
||||
Reference in New Issue
Block a user