fix(build): reject unknown slhx attributes
Fail fast when templates use misspelled or unsupported data-slhx-* attributes, with guidance to fix the spelling or use non-slhx data-* metadata for app-specific islands. req: convention/009 req: diagnostics/002
This commit is contained in:
@@ -884,6 +884,9 @@ All forms support returning `impl IntoEffect` and compose through tuples.
|
||||
### req: convention/008
|
||||
008 `data-slhx-disable-while-pending` disables the triggering form controls or button while the request is active and restores them afterward.
|
||||
|
||||
### req: convention/009
|
||||
009 Unknown `data-slhx-*` authoring attributes are build errors with a suggested fix. slhx-owned attributes are a checked contract, not a silent extension namespace; opaque/integration islands should use explicit allowed attributes or their own non-slhx `data-*` names.
|
||||
|
||||
---
|
||||
|
||||
## multipart
|
||||
|
||||
@@ -150,6 +150,7 @@ impl Resources {
|
||||
};
|
||||
|
||||
reject_selector_target_attrs(path, &node.attrs)?;
|
||||
reject_unknown_slhx_attrs(path, &node.attrs)?;
|
||||
|
||||
if let Some(class_attr) = static_attr(&node.attrs, "class") {
|
||||
for token in class_tokens(&class_attr) {
|
||||
@@ -968,6 +969,49 @@ fn reject_selector_target_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_unknown_slhx_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
|
||||
for attr in attrs {
|
||||
let name = attr.name.as_str();
|
||||
if name.starts_with("data-slhx-") && !known_slhx_attr(name) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: unknown slhx attribute `{name}`; check the spelling or use a non-slhx data-* attribute for app-specific metadata",
|
||||
path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn known_slhx_attr(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"data-slhx-root"
|
||||
| "data-slhx-sse"
|
||||
| "data-slhx-st"
|
||||
| "data-slhx-handle"
|
||||
| "data-slhx-slot"
|
||||
| "data-slhx-form"
|
||||
| "data-slhx-atom"
|
||||
| "data-slhx-key"
|
||||
| "data-slhx-on"
|
||||
| "data-slhx-pending-class"
|
||||
| "data-slhx-indicator"
|
||||
| "data-slhx-confirm"
|
||||
| "data-slhx-debounce"
|
||||
| "data-slhx-throttle"
|
||||
| "data-slhx-every"
|
||||
| "data-slhx-disable-while-pending"
|
||||
| "data-slhx-policy"
|
||||
| "data-slhx-nav"
|
||||
| "data-slhx-boost"
|
||||
| "data-slhx-error-for"
|
||||
| "data-slhx-island"
|
||||
)
|
||||
}
|
||||
|
||||
fn reject_unkeyed_loop(
|
||||
surface: &SurfaceDocument,
|
||||
mut scope: ScopeId,
|
||||
@@ -1394,6 +1438,29 @@ fn main() {{
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_slhx_authoring_attrs() {
|
||||
// req: convention/009 req: diagnostics/002
|
||||
let base = test_dir("slhx-build-unknown-slhx-attr-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("typo.heml"),
|
||||
r#"<button data-slhx-handle="save" data-slhx-pendig-class="busy">Save</button>"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = app().template_dir(&templates).out_dir(&out).run().unwrap_err();
|
||||
assert!(err.to_string().contains("data-slhx-pendig-class"), "missing attr in diagnostic: {err}");
|
||||
assert!(err.to_string().contains("unknown slhx attribute"), "missing unknown-attr diagnostic: {err}");
|
||||
assert!(err.to_string().contains("check the spelling"), "missing spelling guidance: {err}");
|
||||
assert!(err.to_string().contains("non-slhx data-*"), "missing app metadata guidance: {err}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_lowering_injects_progressive_form_handle_and_key() {
|
||||
// req: form/002, req: wire/001
|
||||
|
||||
Reference in New Issue
Block a user