use std::{
env, fs,
path::{Path, PathBuf},
};
fn main() {
if let Err(e) = build() {
eprintln!("â Build failed: {}", e);
std::process::exit(1);
}
}
fn build() -> anyhow::Result<()> {
println!("đ¨ Hikari Builder starting...");
let workspace_root = env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
let Some(workspace_root) = find_workspace_root(&workspace_root) else {
println!("âšī¸ Not in hikari workspace, skipping build steps.");
return Ok(());
};
println!("đ Workspace root: {:?}", workspace_root);
let scss_files = scan_scss_files(&workspace_root)?;
println!("đ Found {} SCSS files", scss_files.len());
let scss_watch_paths = [
"packages/components/src/styles",
"packages/components/src/styles/components",
"packages/theme/styles",
];
for watch_path in &scss_watch_paths {
let full_path = workspace_root.join(watch_path);
if full_path.exists() {
println!("cargo:rerun-if-changed={}", full_path.display());
println!("đī¸ Watching: {:?}", full_path);
if let Ok(entries) = fs::read_dir(&full_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("scss") {
println!("cargo:rerun-if-changed={}", path.display());
}
}
}
}
}
let index_scss = workspace_root.join("packages/components/src/styles/index.scss");
if index_scss.exists() {
println!("cargo:rerun-if-changed={}", index_scss.display());
println!("đī¸ Watching index.scss");
}
let generated_dir = workspace_root.join("packages/builder/src/generated");
fs::create_dir_all(&generated_dir)?;
generate_component_constants(&generated_dir, &scss_files)?;
compile_scss_bundle(&workspace_root)?;
println!("â
Hikari Builder completed!");
Ok(())
}
fn find_workspace_root(start: &Path) -> Option<PathBuf> {
let mut current = start;
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists()
&& let Ok(content) = fs::read_to_string(&cargo_toml)
&& content.contains("[workspace]")
{
return Some(current.to_path_buf());
}
match current.parent() {
Some(parent) if parent != current => {
current = parent;
}
_ => return None,
}
}
}
fn scan_scss_files(workspace_root: &Path) -> anyhow::Result<Vec<String>> {
let mut scss_files = Vec::new();
let components_dir = workspace_root.join("packages/components/src/styles/components");
if components_dir.exists() {
println!("đ Scanning: {:?}", components_dir);
let entries = fs::read_dir(&components_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("scss")
&& let Some(file_name) = path.file_stem().and_then(|s| s.to_str())
{
scss_files.push(file_name.to_string());
println!(" â {}.scss", file_name);
}
}
}
let theme_styles_dir = workspace_root.join("packages/theme/styles");
if theme_styles_dir.exists() {
println!("đ Scanning theme styles: {:?}", theme_styles_dir);
let entries = fs::read_dir(&theme_styles_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("scss")
&& let Some(file_name) = path.file_stem().and_then(|s| s.to_str())
{
scss_files.push(format!("theme-{}", file_name));
println!(" â theme-{}.scss", file_name);
}
}
}
scss_files.sort();
println!("đ Total SCSS files discovered: {}", scss_files.len());
Ok(scss_files)
}
fn generate_component_constants(output_dir: &Path, components: &[String]) -> anyhow::Result<()> {
let mut content = String::from(
r#"//! Auto-generated component list
//!
//! This file is generated by hikari-builder build script.
//! Do not edit manually.
use std::collections::HashSet;
/// List of available Hikari components
pub static AVAILABLE_COMPONENTS: &[&str] = &[
"#,
);
for component in components {
content.push_str(&format!(" \"{}\",\n", component));
}
content.push_str(
r#"];
/// Get default set of components (all available)
pub fn default_components() -> HashSet<String> {
AVAILABLE_COMPONENTS.iter().map(|s| s.to_string()).collect()
}
"#,
);
fs::write(output_dir.join("components.rs"), content)?;
println!("đ Generated components.rs");
Ok(())
}
fn compile_scss_bundle(workspace_root: &Path) -> anyhow::Result<()> {
println!("đ¨ Compiling SCSS with grass...");
let index_scss = workspace_root.join("packages/components/src/styles/index.scss");
if !index_scss.exists() {
return Err(anyhow::anyhow!("index.scss not found at {:?}", index_scss));
}
println!(" Entry point: {:?}", index_scss);
let output_dir = workspace_root.join("public/styles");
fs::create_dir_all(&output_dir)?;
let css_output = output_dir.join("bundle.css");
if css_output.exists() {
fs::remove_file(&css_output)?;
println!("đī¸ Removed old bundle.css");
}
let opts = grass::Options::default()
.style(grass::OutputStyle::Compressed)
.input_syntax(grass::InputSyntax::Scss);
let css_content = grass::from_path(&index_scss, &opts)
.map_err(|e| anyhow::anyhow!("Failed to compile SCSS {:?}: {}", index_scss, e))?;
let file_size = css_content.len();
fs::write(&css_output, &css_content)?;
if css_output.exists() {
println!("â
CSS bundle generated: {:?}", css_output);
println!(" Size: {} bytes", file_size);
println!(
" Verified: File exists at {:?}",
css_output.canonicalize()
);
} else {
return Err(anyhow::anyhow!(
"CSS bundle was not created at {:?}",
css_output
));
}
Ok(())
}