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(
84 features: &[Feature],
85 config: BuildConfig,
86 skip_changes: bool,
87) -> Result<()> {
88 println!(
89 "Creating build in directory: {}",
90 config.output_dir.display()
91 );
92
93 if config.clean && config.output_dir.exists() {
95 println!("Cleaning existing build directory...");
96 fs::remove_dir_all(&config.output_dir).await?;
97 }
98
99 fs::create_dir_all(&config.output_dir).await?;
101
102 extract_embedded_files(&config.output_dir).await?;
104
105 generate_features_json(features, &config.output_dir).await?;
107
108 generate_metadata_json(&config.output_dir, skip_changes).await?;
110
111 println!("Build completed successfully!");
112 println!("Output directory: {}", config.output_dir.display());
113
114 Ok(())
115}
116
117async fn extract_embedded_files(output_dir: &Path) -> Result<()> {
119 println!("Extracting embedded static files...");
120
121 extract_dir_recursive(&STATIC_DIR, output_dir, "").await?;
122
123 Ok(())
124}
125
126fn extract_dir_recursive<'a>(
128 dir: &'a Dir<'a>,
129 output_base: &'a Path,
130 relative_path: &'a str,
131) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
132 Box::pin(async move {
133 let current_output_dir = output_base.join(relative_path);
135 if !relative_path.is_empty() {
136 fs::create_dir_all(¤t_output_dir).await?;
137 }
138
139 for file in dir.files() {
141 let file_path = current_output_dir.join(file.path().file_name().unwrap());
142 println!(" Extracting: {}", file_path.display());
143
144 fs::write(&file_path, file.contents()).await?;
145 }
146
147 for subdir in dir.dirs() {
149 let subdir_name = subdir.path().file_name().unwrap().to_string_lossy();
150 let new_relative_path = if relative_path.is_empty() {
151 subdir_name.to_string()
152 } else {
153 format!("{}/{}", relative_path, subdir_name)
154 };
155
156 extract_dir_recursive(subdir, output_base, &new_relative_path).await?;
157 }
158
159 Ok(())
160 })
161}
162
163async fn generate_features_json(features: &[Feature], output_dir: &Path) -> Result<()> {
165 println!("Generating features.json...");
166
167 let features_json = serde_json::to_string_pretty(features)
168 .map_err(|e| anyhow::anyhow!("Failed to serialize features to JSON: {}", e))?;
169
170 let features_path = output_dir.join("features.json");
171 fs::write(&features_path, features_json).await?;
172
173 println!(" Created: {}", features_path.display());
174
175 Ok(())
176}
177
178async fn generate_metadata_json(output_dir: &Path, skip_changes: bool) -> Result<()> {
180 println!("Generating metadata.json...");
181
182 let metadata = serde_json::json!({
183 "version": env!("CARGO_PKG_VERSION"),
184 "skipChanges": skip_changes
185 });
186
187 let metadata_json = serde_json::to_string_pretty(&metadata)
188 .map_err(|e| anyhow::anyhow!("Failed to serialize metadata to JSON: {}", e))?;
189
190 let metadata_path = output_dir.join("metadata.json");
191 fs::write(&metadata_path, metadata_json).await?;
192
193 println!(" Created: {}", metadata_path.display());
194
195 Ok(())
196}