Skip to main content

features_cli/
build.rs

1//! Build module for creating static file builds from embedded resources.
2//!
3//! This module provides functionality to extract embedded static files and features data
4//! to a build directory, creating a complete static website that can be deployed.
5
6use anyhow::Result;
7use include_dir::{Dir, include_dir};
8use std::path::{Path, PathBuf};
9use tokio::fs;
10
11use crate::models::Feature;
12
13// Embed the public directory at compile time
14static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/public");
15
16/// Configuration for the build process
17#[derive(Debug, Clone)]
18pub struct BuildConfig {
19    /// Output directory for the build
20    pub output_dir: PathBuf,
21    /// Whether to clean the output directory before building
22    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    /// Create a new build configuration with custom output directory
36    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    /// Set whether to clean the output directory before building
44    #[allow(dead_code)]
45    pub fn with_clean(mut self, clean: bool) -> Self {
46        self.clean = clean;
47        self
48    }
49}
50
51/// Creates a static build by extracting embedded files and generating features.json
52///
53/// # Arguments
54///
55/// * `features` - Slice of Feature objects to include in the build
56/// * `config` - Build configuration
57/// * `skip_changes` - Whether changes were skipped during feature computation
58///
59/// # Returns
60///
61/// * `Result<()>` - Ok if build succeeds, Err otherwise
62///
63/// # Build Output
64///
65/// The build directory will contain:
66/// * All static files from the embedded public directory
67/// * `features.json` - Generated features data
68/// * `metadata.json` - Generated metadata with version and skipChanges flag
69///
70/// # Example
71///
72/// ```rust,no_run
73/// use features_cli::build::{create_build, BuildConfig};
74/// use features_cli::models::Feature;
75///
76/// #[tokio::main]
77/// async fn main() -> anyhow::Result<()> {
78///     let features = vec![]; // Your features data
79///     let config = BuildConfig::new("dist");
80///     create_build(&features, config, false).await
81/// }
82/// ```
83pub 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    // Clean output directory if requested
94    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    // Create output directory
100    fs::create_dir_all(&config.output_dir).await?;
101
102    // Extract all embedded static files
103    extract_embedded_files(&config.output_dir).await?;
104
105    // Generate features.json
106    generate_features_json(features, &config.output_dir).await?;
107
108    // Generate metadata.json
109    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
117/// Extracts all embedded static files to the output directory
118async 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
126/// Recursively extracts files from an embedded directory
127fn 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        // Create the current directory in output
134        let current_output_dir = output_base.join(relative_path);
135        if !relative_path.is_empty() {
136            fs::create_dir_all(&current_output_dir).await?;
137        }
138
139        // Extract all files in the current directory
140        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        // Recursively extract subdirectories
148        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
163/// Generates the features.json file
164async 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
178/// Generates the metadata.json file
179async 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}