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///
58/// # Returns
59///
60/// * `Result<()>` - Ok if build succeeds, Err otherwise
61///
62/// # Build Output
63///
64/// The build directory will contain:
65/// * All static files from the embedded public directory
66/// * `features.json` - Generated features data
67///
68/// # Example
69///
70/// ```rust,no_run
71/// use features_cli::build::{create_build, BuildConfig};
72/// use features_cli::models::Feature;
73///
74/// #[tokio::main]
75/// async fn main() -> anyhow::Result<()> {
76///     let features = vec![]; // Your features data
77///     let config = BuildConfig::new("dist");
78///     create_build(&features, config).await
79/// }
80/// ```
81pub async fn create_build(features: &[Feature], config: BuildConfig) -> Result<()> {
82    println!(
83        "Creating build in directory: {}",
84        config.output_dir.display()
85    );
86
87    // Clean output directory if requested
88    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    // Create output directory
94    fs::create_dir_all(&config.output_dir).await?;
95
96    // Extract all embedded static files
97    extract_embedded_files(&config.output_dir).await?;
98
99    // Generate features.json
100    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
108/// Extracts all embedded static files to the output directory
109async 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
117/// Recursively extracts files from an embedded directory
118fn 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        // Create the current directory in output
125        let current_output_dir = output_base.join(relative_path);
126        if !relative_path.is_empty() {
127            fs::create_dir_all(&current_output_dir).await?;
128        }
129
130        // Extract all files in the current directory
131        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        // Recursively extract subdirectories
139        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
154/// Generates the features.json file
155async 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}