feat(build): accept precomputed surface facts

Allow slhx-build callers to feed hemplate SurfaceDocument facts directly, preserving the existing .heml parse fallback while covering the build-from-surface path.

req: surface/008

req: build/001
This commit is contained in:
slhx agent
2026-05-26 01:24:03 +02:00
parent d3fc1a745a
commit 280464017b
+41
View File
@@ -14,6 +14,7 @@ pub struct AppBuilder {
out_dir: Option<PathBuf>,
template_dir: PathBuf,
global_exports: bool,
surfaces: Vec<(PathBuf, SurfaceDocument)>,
}
impl AppBuilder {
@@ -32,6 +33,11 @@ impl AppBuilder {
self
}
pub fn surface(mut self, path: impl Into<PathBuf>, surface: SurfaceDocument) -> Self {
self.surfaces.push((path.into(), surface));
self
}
pub fn run(self) -> io::Result<()> {
println!("cargo:rerun-if-changed={}", self.template_dir.display());
@@ -46,6 +52,7 @@ impl AppBuilder {
std::fs::create_dir_all(&out_dir)?;
let mut resources = Resources::default();
if self.surfaces.is_empty() {
for path in collect_heml(&self.template_dir)? {
let source = std::fs::read_to_string(&path)?;
let doc = build_ast(Arc::new(source)).map_err(|err| parse_error(&path, err))?;
@@ -55,6 +62,12 @@ impl AppBuilder {
let surface = extract_surface(&doc);
resources.add_surface(&self.template_dir, &path, &surface)?;
}
} else {
// req: surface/008
for (path, surface) in &self.surfaces {
resources.add_surface(&self.template_dir, path, surface)?;
}
}
for path in collect_stylesheets(&self.template_dir)? {
let source = std::fs::read_to_string(&path)?;
resources.add_stylesheet(&self.template_dir, &path, &source)?;
@@ -74,6 +87,7 @@ pub fn app() -> AppBuilder {
out_dir: None,
template_dir: PathBuf::from("templates"),
global_exports: false,
surfaces: Vec::new(),
}
}
@@ -1090,6 +1104,33 @@ mod tests {
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn app_can_consume_precomputed_surface_facts() {
// req: surface/008 req: build/001
let base = test_dir("slhx-build-precomputed-surface-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();
let path = templates.join("todo.heml");
let source = Arc::new(
r#"<button data-slhx-handle="create" data-slhx-on="click">Create</button><div data-slhx-slot="todos"></div>"#
.to_string(),
);
let ast = build_ast(source).unwrap().unwrap();
let surface = extract_surface(&ast);
app().template_dir(&templates).out_dir(&out).surface(&path, surface).run().unwrap();
let generated = std::fs::read_to_string(out.join("slhx.generated.rs")).unwrap();
assert!(generated.contains("pub mod todo"));
assert!(generated.contains("pub const create: ::slhx::Handle<()> = ::slhx::Handle::new("));
assert!(generated.contains("pub const todos: ::slhx::Slot<::std::string::String> = ::slhx::Slot::new("));
assert!(generated.contains("pub const click: ::slhx::EventName = ::slhx::EventName::new(\"click\")"));
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn global_resource_exports_are_explicit_opt_in() {
// req: component/003