website-htmx-rustelo-code/crates/shared/build.rs
2026-07-10 03:44:13 +01:00

222 lines
8.2 KiB
Rust

//! Shared build script - Using Provider pattern with Rustelo foundation traits
//!
//! This build script uses the Provider pattern with dependency injection
//! instead of direct function calls. Follows PAP principles completely.
//!
//! Includes build-time route validation to ensure menu routes match configured pages.
/// Walk up from `CARGO_MANIFEST_DIR` to find the Cargo workspace root.
///
/// Falls back to `CARGO_MANIFEST_DIR` itself when no ancestor contains `[workspace]`,
/// which is correct for single-crate projects.
fn find_workspace_root() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
let pkg_dir = std::env::var("CARGO_MANIFEST_DIR")
.map_err(|_| "CARGO_MANIFEST_DIR not set — this function is only valid in build scripts")?;
let mut current = std::path::PathBuf::from(pkg_dir);
for _ in 0..15 {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
return Ok(current);
}
}
}
current = current
.parent()
.ok_or("Reached filesystem root without finding a [workspace] Cargo.toml")?
.to_path_buf();
}
Err("Workspace root not found — ensure a Cargo.toml with [workspace] exists in the project hierarchy".into())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use new PAP-compliant manifest system
// let _manifest = rustelo_utils::get_manifest();
let content_path = rustelo_utils::manifest_content_path();
let config_path = rustelo_utils::manifest_config_path();
let routes_path = rustelo_utils::routes_path();
let ui_path = rustelo_utils::ui_path();
println!("cargo:rerun-if-changed={}", routes_path.display());
println!("cargo:rerun-if-changed={}", config_path.display());
println!("cargo:rerun-if-changed={}", content_path.display());
println!("cargo:rerun-if-changed={}", ui_path.display());
// Trigger rebuild when unified site config changes.
// ManifestResolver is preferred; if it fails (no rustelo.manifest.toml), derive from CARGO_MANIFEST_DIR.
let manifest_dir = rustelo_utils::ManifestResolver::load()
.map(|m| m.root().to_path_buf())
.or_else(|_| find_workspace_root())?;
let site_ncl = manifest_dir.join("site").join("config").join("index.ncl");
if site_ncl.exists() {
println!("cargo:rerun-if-changed={}", site_ncl.display());
}
// Generate shared route constants
generate_shared_routes()?;
// Validate menu routes match configured routes
validate_menu_routes(&routes_path, &ui_path)?;
println!("cargo:warning=Shared build completed");
Ok(())
}
fn generate_shared_routes() -> Result<(), Box<dyn std::error::Error>> {
use std::collections::HashMap;
use std::path::Path;
use std::{env, fs};
let mut all_routes: HashMap<String, HashMap<String, String>> = HashMap::new();
let manifest_dir = rustelo_utils::ManifestResolver::load()
.map(|m| m.root().to_path_buf())
.or_else(|_| find_workspace_root())
.expect("Cannot determine workspace root — check CARGO_MANIFEST_DIR or add rustelo.manifest.toml");
let site_cfg = build_config::site_config::load_unified_site_config(&manifest_dir)
.unwrap_or_else(|e| {
panic!(
"Cannot load site configuration: {e}\n\
Install Nickel: cargo install nickel-lang-cli"
)
});
println!(
"cargo:warning=Shared: Loaded site configuration ({} languages)",
site_cfg.languages().len()
);
for (lang, routes) in site_cfg.routes_expanded() {
let mut lang_routes = HashMap::new();
for route in routes {
lang_routes.insert(route.component.to_lowercase(), route.path.clone());
}
all_routes.insert(lang, lang_routes);
}
// Generate Rust code for route constants
let mut generated_code = String::new();
generated_code.push_str("// Auto-generated route constants\n");
generated_code.push_str("// Generated from manifest routes\n\n");
generated_code.push_str("pub mod routes {\n");
for (lang, routes) in &all_routes {
generated_code.push_str(&format!(" pub mod {} {{\n", lang));
for (route_name, path) in routes {
let const_name = route_name.to_uppercase();
generated_code.push_str(&format!(
" pub const {}: &str = \"{}\";\n",
const_name, path
));
}
generated_code.push_str(" }\n\n");
}
generated_code.push_str("}\n");
// Write the generated routes
let out_dir = env::var("OUT_DIR")?;
let routes_file = Path::new(&out_dir).join("routes_generated.rs");
fs::write(routes_file, generated_code)?;
Ok(())
}
/// Validate that all menu routes point to configured pages
fn validate_menu_routes(
routes_path: &std::path::Path,
ui_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
use std::collections::{HashMap, HashSet};
use std::fs;
let menus_path = ui_path.join("menus");
// If menus directory doesn't exist, skip validation
if !menus_path.exists() {
return Ok(());
}
// Load all configured routes by language using typed loading
let mut configured_routes: HashMap<String, HashSet<String>> = HashMap::new();
if routes_path.exists() {
// Use typed route loading (NCL-first, TOML-fallback)
match build_config::route_config::load_all_routes(routes_path) {
Ok(routes_by_lang) => {
for (lang, routes) in routes_by_lang {
let mut lang_routes = HashSet::new();
for route in routes {
// Collect all route paths for validation
lang_routes.insert(route.path.clone());
}
configured_routes.insert(lang, lang_routes);
}
}
Err(e) => {
println!(
"cargo:warning=Failed to load routes for menu validation: {}",
e
);
// Continue without validation if routes can't be loaded
return Ok(());
}
}
}
// Create route validator
let validator = rustelo_utils::RouteValidator::new(configured_routes);
// Validate all menu files
for entry in fs::read_dir(&menus_path)? {
let entry = entry?;
if entry.path().extension().is_some_and(|ext| ext == "toml") {
let path = entry.path();
if let Some(language) = path.file_stem().and_then(|s| s.to_str()) {
let menu_content = fs::read_to_string(&path)?;
let menu: toml::Value = toml::from_str(&menu_content)?;
if let Some(menu_items) = menu.get("menu").and_then(|v| v.as_array()) {
let mut route_checks = Vec::new();
for menu_item in menu_items {
if let Some(route) = menu_item.get("route").and_then(|v| v.as_str()) {
let is_external = menu_item
.get("is_external")
.and_then(|v| v.as_bool())
.unwrap_or(false);
route_checks.push((
route.to_string(),
language.to_string(),
is_external,
));
}
}
// Validate batch
if let Err(errors) = validator.validate_batch(&route_checks) {
let error_messages = errors
.iter()
.map(|e| format!(" - {}", e))
.collect::<Vec<_>>()
.join("\n");
println!(
"cargo:warning=Menu validation errors in {}:\n{}",
path.display(),
error_messages
);
}
}
}
}
}
Ok(())
}