diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index b7909d2..070ccff 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -166,8 +166,8 @@ slhx competes with React by making frontend frameworks unnecessary for most apps ### req: component/003 003 Generated APIs are component-namespaced by default: -`ui::slots::todo_row`, `ui::handles::create`, `ui::forms::create`. -Global exports are opt-in only. +`ui::todo_list::slots::todo_row`, `ui::todo_list::handles::create`, `ui::todo_list::forms::create`, and `ui::todo_list::COMPONENT` as a checked `ComponentRef`. +Global exports (`ui::slots::*`, `ui::handles::*`, `ui::components::*`) are opt-in only. ### req: component/004 004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source. diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index fe8c3f9..1a5c459 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -330,6 +330,7 @@ impl Resources { // req: build/001 req: component/003 if global_exports { + self.push_component_refs(&mut out, 0); self.push_resource_modules(&mut out, None, 0); self.push_lowering_api(&mut out, None, 0); } @@ -346,11 +347,31 @@ impl Resources { out } + fn push_component_refs(&self, out: &mut String, indent: usize) { + let pad = " ".repeat(indent); + let inner = " ".repeat(indent + 1); + out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod components {{\n")); + for component in self.component_names() { + out.push_str(&format!( + "{inner}pub const {component}: ::slhx::ComponentRef = ::slhx::ComponentRef::new({});\n", + rust_str(&component) + )); + } + out.push_str(&format!("{pad}}}\n")); + } + fn push_resource_modules(&self, out: &mut String, component: Option<&str>, indent: usize) { let pad = " ".repeat(indent); let inner = " ".repeat(indent + 1); let mut handle_ids = Vec::new(); + if let Some(component) = component { + out.push_str(&format!( + "{pad}pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new({});\n", + rust_str(component) + )); + } + out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod slots {{\n")); for res in self.slots.values().filter(|res| component_matches(res, component)) { if res.keyed { @@ -1035,6 +1056,8 @@ mod tests { app().template_dir(&templates).out_dir(&out).global_exports(true).run().unwrap(); let generated = std::fs::read_to_string(out.join("slhx.generated.rs")).unwrap(); + assert!(generated.contains("pub mod components")); + assert!(generated.contains("pub const todo: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")")); assert!(generated.contains("pub mod slots")); assert!(generated.contains("pub const todos")); assert!(generated.contains("pub mod handles")); @@ -1084,9 +1107,11 @@ mod tests { app().template_dir(&templates).out_dir(&out).run().unwrap(); let generated = std::fs::read_to_string(out.join("slhx.generated.rs")).unwrap(); + assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod components")); assert!(!generated.contains("\n#[allow(non_upper_case_globals)]\npub mod slots")); assert!(!generated.contains("\npub fn lower_html")); assert!(generated.contains("pub mod todo")); + assert!(generated.contains(" pub const COMPONENT: ::slhx::ComponentRef = ::slhx::ComponentRef::new(\"todo\")")); assert!(generated.contains(" pub mod slots")); assert!(generated.contains(" pub fn lower_html")); @@ -1201,6 +1226,8 @@ mod slhx {{ impl Form {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} #[derive(Clone, Copy)] pub struct CssClass(&'static str); impl CssClass {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} pub const fn as_str(self) -> &'static str {{ self.0 }} }} + #[derive(Clone, Copy)] pub struct ComponentRef(&'static str); + impl ComponentRef {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} pub const fn as_str(self) -> &'static str {{ self.0 }} }} pub struct FormContract {{ pub fields: &'static [FormField] }} pub struct FormField {{ pub name: &'static str, pub kind: FormControlKind, pub required: bool }} pub enum FormControlKind {{ Text, Number {{ min: Option<&'static str>, max: Option<&'static str>, step: Option<&'static str> }}, Checkbox, Radio, Select {{ multiple: bool }}, TextArea, File, Hidden, Submit, Other {{ tag: &'static str, input_type: Option<&'static str> }} }} @@ -1292,6 +1319,8 @@ mod slhx {{ impl Atom {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} #[derive(Clone, Copy)] pub struct Form(::std::marker::PhantomData); impl Form {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} + #[derive(Clone, Copy)] pub struct ComponentRef(&'static str); + impl ComponentRef {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} pub const fn as_str(self) -> &'static str {{ self.0 }} }} pub struct FormContract {{ pub fields: &'static [FormField] }} pub struct FormField {{ pub name: &'static str, pub kind: FormControlKind, pub required: bool }} pub enum FormControlKind {{ Text, Number {{ min: Option<&'static str>, max: Option<&'static str>, step: Option<&'static str> }}, Checkbox, Radio, Select {{ multiple: bool }}, TextArea, File, Hidden, Submit, Other {{ tag: &'static str, input_type: Option<&'static str> }} }} diff --git a/slhx-core/src/lib.rs b/slhx-core/src/lib.rs index 7a6b8d9..d9e0a12 100644 --- a/slhx-core/src/lib.rs +++ b/slhx-core/src/lib.rs @@ -186,6 +186,35 @@ impl core::fmt::Display for CssClass { } } +/// A generated, checked component token. +/// req: component/003 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct ComponentRef { + name: &'static str, +} + +impl ComponentRef { + pub const fn new(name: &'static str) -> Self { + Self { name } + } + + pub const fn as_str(self) -> &'static str { + self.name + } +} + +impl AsRef for ComponentRef { + fn as_ref(&self) -> &str { + self.name + } +} + +impl core::fmt::Display for ComponentRef { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.name) + } +} + /// A generated, checked event token. /// req: codegen/006 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] diff --git a/slhx-core/tests/effect_batch.rs b/slhx-core/tests/effect_batch.rs index 2f8a97f..0aa95c8 100644 --- a/slhx-core/tests/effect_batch.rs +++ b/slhx-core/tests/effect_batch.rs @@ -1,7 +1,7 @@ use slhx_core::{ event, navigate, redirect, replace, Atom, AtomSnapshot, AtomState, BuildFingerprint, - CssClass, CssClasses, Effect, EffectBatch, Form, Handle, IntoEffect, KeyedSlot, NavigateMode, - Payload, ResourceKind, SafeHtml, ScopeKey, Slot, + ComponentRef, CssClass, CssClasses, Effect, EffectBatch, Form, Handle, IntoEffect, KeyedSlot, + NavigateMode, Payload, ResourceKind, SafeHtml, ScopeKey, Slot, }; #[test] @@ -93,6 +93,14 @@ fn slot_html_requires_explicit_safe_html() { assert_eq!(payload, Payload::Html(String::from("ok"))); } +#[test] +fn component_refs_format_generated_component_names() { + // req: component/003 + let component = ComponentRef::new("todo_list"); + assert_eq!(component.as_str(), "todo_list"); + assert_eq!(component.to_string(), "todo_list"); +} + #[test] fn generated_handles_format_for_hemplate_dynamic_handle_attrs() { // req: public_api/001 req: public_api/002 diff --git a/slhx/src/lib.rs b/slhx/src/lib.rs index d712105..6df598f 100644 --- a/slhx/src/lib.rs +++ b/slhx/src/lib.rs @@ -8,8 +8,9 @@ pub use slhx_derive::{app, component, form, handler, surface}; pub mod prelude { pub use slhx_core::{ - navigate, push, redirect, replace, Atom, BuildFingerprint, CssClass, CssClasses, Effect, - EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot, Slot, + navigate, push, redirect, replace, Atom, BuildFingerprint, ComponentRef, CssClass, + CssClasses, Effect, EventName, Form, FormModel, FormValue, Handle, IntoEffect, KeyedSlot, + Slot, }; pub use slhx_derive::{app, component, form, handler, surface}; }