1use anyhow::Result;
7use include_dir::{Dir, include_dir};
8use std::path::{Path, PathBuf};
9use tokio::fs;
10
11use crate::models::Feature;
12
13static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/public");
15
16#[derive(Debug, Clone)]
18pub struct BuildConfig {
19 pub output_dir: PathBuf,
21 pub clean: bool,
23}
24
25impl Default for BuildConfig {
26 fn default() -> Self {
27 Self {
28 output_dir: PathBuf::from("build"),
29 clean: true,
30 }
31 }
32}
33
34impl BuildConfig {
35 pub fn new<P: Into<PathBuf>>(output_dir: P) -> Self {
37 Self {
38 output_dir: output_dir.into(),
39 ..Default::default()
40 }
41 }
42
43 #[allow(dead_code)]
45 pub fn with_clean(mut self, clean: bool) -> Self {
46 self.clean = clean;
47 self
48 }
49}
50
51pub async fn create_build(features: &[Feature], config: BuildConfig) -> Result<()> {
82 println!(
83 "Creating build in directory: {}",
84 config.output_dir.display()
85 );
86
87 if config.clean && config.output_dir.exists() {
89 println!("Cleaning existing build directory...");
90 fs::remove_dir_all(&config.output_dir).await?;
91 }
92
93 fs::create_dir_all(&config.output_dir).await?;
95
96 extract_embedded_files(&config.output_dir).await?;
98
99 generate_features_json(features, &config.output_dir).await?;
101
102 println!("Build completed successfully!");
103 println!("Output directory: {}", config.output_dir.display());
104
105 Ok(())
106}
107
108async fn extract_embedded_files(output_dir: &Path) -> Result<()> {
110 println!("Extracting embedded static files...");
111
112 extract_dir_recursive(&STATIC_DIR, output_dir, "").await?;
113
114 Ok(())
115}
116
117fn extract_dir_recursive<'a>(
119 dir: &'a Dir<'a>,
120 output_base: &'a Path,
121 relative_path: &'a str,
122) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
123 Box::pin(async move {
124 let current_output_dir = output_base.join(relative_path);
126 if !relative_path.is_empty() {
127 fs::create_dir_all(¤t_output_dir).await?;
128 }
129
130 for file in dir.files() {
132 let file_path = current_output_dir.join(file.path().file_name().unwrap());
133 println!(" Extracting: {}", file_path.display());
134
135 fs::write(&file_path, file.contents()).await?;
136 }
137
138 for subdir in dir.dirs() {
140 let subdir_name = subdir.path().file_name().unwrap().to_string_lossy();
141 let new_relative_path = if relative_path.is_empty() {
142 subdir_name.to_string()
143 } else {
144 format!("{}/{}", relative_path, subdir_name)
145 };
146
147 extract_dir_recursive(subdir, output_base, &new_relative_path).await?;
148 }
149
150 Ok(())
151 })
152}
153
154async fn generate_features_json(features: &[Feature], output_dir: &Path) -> Result<()> {
156 println!("Generating features.json...");
157
158 let features_json = serde_json::to_string_pretty(features)
159 .map_err(|e| anyhow::anyhow!("Failed to serialize features to JSON: {}", e))?;
160
161 let features_path = output_dir.join("features.json");
162 fs::write(&features_path, features_json).await?;
163
164 println!(" Created: {}", features_path.display());
165
166 Ok(())
167}