fix(build): validate slhx convention placement
Reject data-slhx-nav on non-anchor elements and data-slhx-sse away from its root, so standard conventions keep progressive navigation and root-scoped push semantics instead of silently doing nothing. req: page_swap/001 req: push/006 req: diagnostics/002
This commit is contained in:
+1
-1
@@ -485,7 +485,7 @@ what a valid business email is.
|
|||||||
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
|
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
|
||||||
|
|
||||||
### req: push/006
|
### req: push/006
|
||||||
006 `data-slhx-sse` opens only non-empty same-origin SSE URLs by default. Empty static URLs are build errors; cross-origin streams belong to explicit integration code rather than the standard runtime convention.
|
006 `data-slhx-sse` is declared on `data-slhx-root` and opens only non-empty same-origin SSE URLs by default. Empty static URLs and non-root placement are build errors; cross-origin streams belong to explicit integration code rather than the standard runtime convention.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ impl Resources {
|
|||||||
reject_selector_target_attrs(path, &node.attrs)?;
|
reject_selector_target_attrs(path, &node.attrs)?;
|
||||||
reject_unknown_slhx_attrs(path, &node.attrs)?;
|
reject_unknown_slhx_attrs(path, &node.attrs)?;
|
||||||
reject_invalid_slhx_attr_values(path, &node.attrs)?;
|
reject_invalid_slhx_attr_values(path, &node.attrs)?;
|
||||||
|
reject_invalid_slhx_attr_placement(path, tag, &node.attrs)?;
|
||||||
|
|
||||||
if let Some(class_attr) = static_attr(&node.attrs, "class") {
|
if let Some(class_attr) = static_attr(&node.attrs, "class") {
|
||||||
for token in class_tokens(&class_attr) {
|
for token in class_tokens(&class_attr) {
|
||||||
@@ -1065,6 +1066,38 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reject_invalid_slhx_attr_placement(path: &Path, tag: &str, attrs: &[SurfaceAttribute]) -> io::Result<()> {
|
||||||
|
if has_attr(attrs, "data-slhx-nav") && tag != "a" {
|
||||||
|
return Err(invalid_slhx_placement(
|
||||||
|
path,
|
||||||
|
"data-slhx-nav",
|
||||||
|
"expected a real `<a href=...>` link so navigation works without JavaScript",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if has_attr(attrs, "data-slhx-sse") && !has_attr(attrs, "data-slhx-root") {
|
||||||
|
return Err(invalid_slhx_placement(
|
||||||
|
path,
|
||||||
|
"data-slhx-sse",
|
||||||
|
"expected placement on the same element as `data-slhx-root`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_attr(attrs: &[SurfaceAttribute], name: &str) -> bool {
|
||||||
|
attrs.iter().any(|attr| attr.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_slhx_placement(path: &Path, attr: &str, expectation: &str) -> io::Error {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"{}: invalid {attr} placement; {expectation}",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn valid_policy(value: &str) -> bool {
|
fn valid_policy(value: &str) -> bool {
|
||||||
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
|
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
|
||||||
}
|
}
|
||||||
@@ -1609,6 +1642,39 @@ fn main() {{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_slhx_attr_placement() {
|
||||||
|
// req: page_swap/001 req: push/006 req: diagnostics/002
|
||||||
|
for (case, template, attr, guidance) in [
|
||||||
|
(
|
||||||
|
"nav-button",
|
||||||
|
r#"<button data-slhx-nav="" data-slhx-handle="go">Go</button>"#,
|
||||||
|
"data-slhx-nav",
|
||||||
|
"<a href=...>",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sse-child",
|
||||||
|
r#"<section data-slhx-root="feed"><div data-slhx-sse="/events"></div></section>"#,
|
||||||
|
"data-slhx-sse",
|
||||||
|
"data-slhx-root",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let base = test_dir(&format!("slhx-build-invalid-placement-{case}-test"));
|
||||||
|
let templates = base.join("templates");
|
||||||
|
let out = base.join("out");
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(&templates).unwrap();
|
||||||
|
std::fs::write(templates.join("placement.heml"), template).unwrap();
|
||||||
|
|
||||||
|
let err = app().template_dir(&templates).out_dir(&out).run().unwrap_err();
|
||||||
|
assert!(err.to_string().contains(attr), "missing attr in diagnostic: {err}");
|
||||||
|
assert!(err.to_string().contains("invalid"), "missing invalid-placement diagnostic: {err}");
|
||||||
|
assert!(err.to_string().contains(guidance), "missing placement guidance {guidance:?}: {err}");
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_lowering_injects_progressive_form_handle_and_key() {
|
fn generated_lowering_injects_progressive_form_handle_and_key() {
|
||||||
// req: form/002, req: wire/001
|
// req: form/002, req: wire/001
|
||||||
|
|||||||
Reference in New Issue
Block a user