From 8fd333090de2e2b9cb94262c49ca09773e46a982 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Mon, 13 Jul 2026 10:56:04 +0200 Subject: [PATCH] fix(derive): suggest valid component scopes --- AGENTS.md | 2 +- REQUIREMENTS.md | 2 +- hemx-derive/src/lib.rs | 28 +++++++++++++++++++++++++++- hemx-derive/tests/compile_fail.rs | 4 ++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0e0b6ea..c0174d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,7 +57,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file - Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013 - hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata and postcard/form-encoded envelopes, not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010 - Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008 -- Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005 +- Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: component/005 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005 - Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry postcard EffectBatch over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002 - `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008 - Use `cargo run -p hemx-xtask -- app new PATH` for the generic page/form/keyed-row/notice starter, and `cargo run -p hemx-xtask -- app new --mobile PATH` for the phone-first starter with host capabilities, recovery truth, and release-kit commands; do not treat it as a mobile framework or store-submission bot. req: ceremony/005 req: ceremony/006 req: ceremony/007 diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 098cb9c..74af340 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -272,7 +272,7 @@ Category modules such as `targets`, `handles`, and `forms` remain available for 0 004 `#[hemx::surface]` bridges generated code into a user module. Users write `#[hemx::surface] mod ui {}` instead of direct `$OUT_DIR` includes; hemx-build emits `hemx.generated.rs` for the macro to expand in place. [north_star] ### req: component/005 -0 005 Optional `#[hemx::component]` validates that each template Surface handle has a corresponding `#[hemx::handler]` within the annotated module. It is strictly module-local, with cross-handler visibility only inside that module. [north_star] +0 005 Optional `#[hemx::component]` validates that each template Surface handle has a corresponding `#[hemx::handler]` within the annotated module. It is strictly module-local, with cross-handler visibility only inside that module; unknown scopes report the available generated component names. [north_star] ### req: component/007 0 007 Missing handlers without `#[hemx::component]` are caught at app mount or test time, not `cargo check`. [north_star] diff --git a/hemx-derive/src/lib.rs b/hemx-derive/src/lib.rs index a3fc000..3d867e8 100644 --- a/hemx-derive/src/lib.rs +++ b/hemx-derive/src/lib.rs @@ -723,8 +723,14 @@ fn component_contract_errors( let mut errors = Vec::new(); if let Some(component) = component.filter(|_| !implemented.is_empty() && generated.is_empty()) { + let available = syms_components(path); + let repair = if available.is_empty() { + "no generated components with handles are available; add a data-hemx-handle to the component template or check build.rs generation".to_owned() + } else { + format!("available generated components: {}", available.join(", ")) + }; errors.push(format!( - "#[hemx::component({component:?})] does not match any generated handles; check the .heml file name or component name" + "#[hemx::component({component:?})] does not match any generated handles; {repair}" )); } @@ -860,6 +866,26 @@ fn syms_handles(path: &PathBuf, component: Option<&str>) -> Vec { .collect() } +fn syms_components(path: &PathBuf) -> Vec { + let Ok(syms) = std::fs::read_to_string(path) else { + return Vec::new(); + }; + let mut components = syms + .lines() + .filter_map(|line| { + let mut fields = line.split('\t'); + matches!(fields.next(), Some("handle")) + .then(|| fields.next()) + .flatten() + .and_then(symbol_component) + .map(ToOwned::to_owned) + }) + .collect::>(); + components.sort_unstable(); + components.dedup(); + components +} + fn symbol_component(symbol: &str) -> Option<&str> { let path = symbol.split_once("::")?.0; path.rsplit_once('/') diff --git a/hemx-derive/tests/compile_fail.rs b/hemx-derive/tests/compile_fail.rs index f9fd75d..d3d1ef0 100644 --- a/hemx-derive/tests/compile_fail.rs +++ b/hemx-derive/tests/compile_fail.rs @@ -223,6 +223,10 @@ mod handlers { stderr.contains("#[hemx::component(\"todos\")] does not match any generated handles"), "missing unknown component diagnostic in stderr:\n{stderr}" ); + assert!( + stderr.contains("available generated components: admin"), + "unknown scope diagnostic must name the valid repair choice:\n{stderr}" + ); } #[test]