From 3c4c802731ec2b2638e60e48ed8031c2654cbf91 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 1 Jun 2026 20:33:33 +0200 Subject: [PATCH] fix(build): reject slhx selector targeting Fail fast when templates invent selector-style data-slhx-target or data-slhx-select attributes, and point authors back to generated local slots instead of runtime selector semantics. req: locality/001 req: locality/002 req: htmx_equivalents/003 --- slhx-build/src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index b19a664..94dc7ed 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -149,6 +149,8 @@ impl Resources { continue; }; + reject_selector_target_attrs(path, &node.attrs)?; + if let Some(class_attr) = static_attr(&node.attrs, "class") { for token in class_tokens(&class_attr) { let canonical = canonical_symbol(root, path, token); @@ -950,6 +952,22 @@ fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool { } } +fn reject_selector_target_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> { + for attr in attrs { + let name = attr.name.as_str(); + if matches!(name, "data-slhx-target" | "data-slhx-select") { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "{}: `{name}` is selector-style targeting; slhx uses generated resources instead. Add data-slhx-slot to the local element and return an effect for that generated slot.", + path.display() + ), + )); + } + } + Ok(()) +} + fn reject_unkeyed_loop( surface: &SurfaceDocument, mut scope: ScopeId, @@ -1345,6 +1363,37 @@ fn main() {{ } } + #[test] + fn rejects_selector_style_targeting_attrs() { + // req: locality/001 req: locality/002 req: htmx_equivalents/003 + for (case, template, attr) in [ + ( + "slhx-target", + r##""##, + "data-slhx-target", + ), + ( + "slhx-select", + r##""##, + "data-slhx-select", + ), + ] { + let base = test_dir(&format!("slhx-build-selector-target-{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("target.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("selector-style targeting"), "missing selector diagnostic: {err}"); + assert!(err.to_string().contains("data-slhx-slot"), "missing suggested slot fix: {err}"); + + let _ = std::fs::remove_dir_all(&base); + } + } + #[test] fn generated_lowering_injects_progressive_form_handle_and_key() { // req: form/002, req: wire/001