feat(api): streamline generated app authoring

Move the canonical examples toward generated component-root helpers, typed form decoding, async/state handler registration, and derive-driven app/component registry wiring. Tighten requirements and diagnostics for the server-first, selectorless authoring path.

Verified with cargo run -p slhx-xtask -- test, cargo check --workspace, redgate list, redgate refs, redgate health --strict, and git diff --check.

req: canonical/001

req: canonical/003

req: canonical/004

req: dx/002

req: derive_app/001

req: component/003

req: form/004

req: axum_integration/003
This commit is contained in:
slhx agent
2026-06-05 06:33:41 +02:00
parent eb6086616c
commit d4e865ef92
34 changed files with 4573 additions and 1156 deletions
+103 -2
View File
@@ -66,6 +66,18 @@ fn canonical_examples_do_not_author_low_level_resource_plumbing() {
"HandlerRegistry",
"InteractionForm",
"register_handle(",
"SafeHtml",
"slhx::advanced",
"advanced::slots",
"KeyedSlot",
"Slot<",
"Effect::",
"Payload::",
"NavigateMode",
"ScopeKey",
"opcode",
"wire format",
"manual generated",
"RenderSlotExt",
"RenderKeyedSlotExt",
".render(&",
@@ -111,6 +123,41 @@ fn canonical_examples_do_not_author_low_level_resource_plumbing() {
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn public_prelude_does_not_export_low_level_primitives() {
// req: public_api/005 req: dx/006
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let facade = std::fs::read_to_string(root.join("slhx/src/lib.rs")).unwrap();
let prelude = facade
.split("pub mod prelude {")
.nth(1)
.and_then(|tail| tail.split("\n}").next())
.expect("slhx facade exposes prelude module");
let exported = prelude
.split(|c: char| !(c == '_' || c.is_ascii_alphanumeric()))
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
for token in [
"SafeHtml",
"Effect",
"Slot",
"KeyedSlot",
"ResourceId",
"EffectBatch",
"BuildFingerprint",
] {
assert!(
!exported.contains(&token),
"slhx::prelude must not export low-level `{token}`"
);
}
assert!(prelude.contains("Html"), "slhx::prelude should export Html");
assert!(
prelude.contains("IntoEffect"),
"slhx::prelude should export IntoEffect"
);
}
#[test]
fn canonical_example_docs_do_not_teach_low_level_plumbing() {
// req: dx/002 req: dx/003 req: examples/003
@@ -130,13 +177,63 @@ fn canonical_example_docs_do_not_teach_low_level_plumbing() {
"register_handle(",
"lower_html(",
"render_html(",
"SafeHtml::trusted",
"SafeHtml",
"KeyedSlot",
"Slot<",
"Effect::",
"opcode",
"wire format",
"manual generated",
"Effect::batch",
"Effect::class",
"Effect::move",
"Effect::set",
"Effect::broadcast",
"Effect::ack",
"SyncEffect::",
"postcard DOM ops",
"data-hid",
"data-sid",
".render(&",
"addEventListener(",
"querySelector",
"querySelectorAll",
];
let mut failures = Vec::new();
scan_examples(&examples, &mut |path, text| {
if path.extension().and_then(|ext| ext.to_str()) != Some("md")
|| is_advanced_boundary_doc(text)
{
return;
}
for (line_no, line) in text.lines().enumerate() {
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
failures.push(format!(
"{}:{}: example docs must teach generated ergonomic APIs, not `{token}`",
path.display(),
line_no + 1
));
}
}
});
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn example_docs_do_not_show_runtime_metadata_as_authoring_contract() {
// req: dx/006 req: misc/006
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let examples = root.join("examples");
let forbidden = [
"data-hid",
"data-sid",
"data-aid",
"data-fid",
"[data-hid",
"[data-sid",
];
let mut failures = Vec::new();
scan_examples(&examples, &mut |path, text| {
if path.extension().and_then(|ext| ext.to_str()) != Some("md") {
return;
@@ -144,7 +241,7 @@ fn canonical_example_docs_do_not_teach_low_level_plumbing() {
for (line_no, line) in text.lines().enumerate() {
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
failures.push(format!(
"{}:{}: example docs must teach generated ergonomic APIs, not `{token}`",
"{}:{}: example docs must describe generated authoring APIs, not runtime metadata `{token}`",
path.display(),
line_no + 1
));
@@ -188,6 +285,10 @@ fn scan_examples(dir: &Path, visit: &mut impl FnMut(&Path, &str)) {
}
}
fn is_advanced_boundary_doc(text: &str) -> bool {
text.contains("advanced/low-level north-star boundary sketch")
}
fn is_example_source(path: &PathBuf) -> bool {
matches!(
path.extension().and_then(|ext| ext.to_str()),