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
This commit is contained in:
@@ -149,6 +149,8 @@ impl Resources {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reject_selector_target_attrs(path, &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) {
|
||||||
let canonical = canonical_symbol(root, path, token);
|
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(
|
fn reject_unkeyed_loop(
|
||||||
surface: &SurfaceDocument,
|
surface: &SurfaceDocument,
|
||||||
mut scope: ScopeId,
|
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##"<button data-slhx-handle="save" data-slhx-target="#row">Save</button>"##,
|
||||||
|
"data-slhx-target",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"slhx-select",
|
||||||
|
r##"<button data-slhx-handle="save" data-slhx-select="closest tr">Save</button>"##,
|
||||||
|
"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]
|
#[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