Skip to main content

mini_static/
css_bundler.rs

1use std::path::{Path, PathBuf};
2use std::fs;
3
4use crate::bundle;
5use crate::minify;
6
7/// Bundle all CSS files from a source directory into a single output file.
8///
9/// Discovers all `.css` files in `src_dir`, reads and concatenates them (in sorted order),
10/// follows `@import` statements within that directory, minifies the result, and writes it
11/// to `output_path`.
12///
13/// The bundling process:
14/// 1. Finds all `.css` files in src_dir (recursively, sorted by path)
15/// 2. Reads and concatenates all CSS file contents
16/// 3. Writes concatenated content to a temporary file
17/// 4. Runs that through the bundler to resolve imports and minify
18/// 5. Writes the final bytes to output_path
19///
20/// # Errors
21///
22/// Returns `Err` if:
23/// - src_dir cannot be read
24/// - output_path cannot be written to
25/// - CSS parsing or bundling fails
26pub async fn bundle_directory_css(
27    src_dir: &Path,
28    output_path: &Path,
29) -> Result<(), CssBundlerError> {
30    let src_dir_canon = src_dir.canonicalize().map_err(|e| {
31        CssBundlerError::ReadSource {
32            path: src_dir.to_path_buf(),
33            reason: e.to_string(),
34        }
35    })?;
36
37    let css_files = find_css_files(&src_dir_canon).map_err(|e| {
38        CssBundlerError::ReadSource {
39            path: src_dir.to_path_buf(),
40            reason: e.to_string(),
41        }
42    })?;
43
44    if css_files.is_empty() {
45        return Err(CssBundlerError::NoFilesFound(src_dir.to_path_buf()));
46    }
47
48    let mut bundled_content = String::new();
49
50    for css_file in &css_files {
51        let (bytes, _deps) = bundle::bundle_and_minify_css(&[src_dir_canon.clone()], css_file)
52            .await
53            .map_err(|e| CssBundlerError::Bundle(format!("{:?}", e)))?;
54
55        bundled_content.push_str(&String::from_utf8_lossy(&bytes));
56    }
57
58    let bundled_bytes_final = minify::minify(bundled_content.as_bytes(), crate::reload::ChangeType::Css)
59        .map_err(|e| CssBundlerError::Bundle(format!("Minification failed: {:?}", e)))?;
60
61    if let Some(parent) = output_path.parent() {
62        fs::create_dir_all(parent).map_err(|e| {
63            CssBundlerError::WriteOutput {
64                path: output_path.to_path_buf(),
65                reason: e.to_string(),
66            }
67        })?;
68    }
69
70    fs::write(output_path, &bundled_bytes_final).map_err(|e| {
71        CssBundlerError::WriteOutput {
72            path: output_path.to_path_buf(),
73            reason: e.to_string(),
74        }
75    })?;
76
77    Ok(())
78}
79
80/// Find all `.css` files in a directory tree.
81fn find_css_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
82    let mut css_files = Vec::new();
83    let mut dirs = vec![dir.to_path_buf()];
84
85    while let Some(current_dir) = dirs.pop() {
86        let entries = fs::read_dir(&current_dir)?;
87        for entry in entries {
88            let entry = entry?;
89            let path = entry.path();
90            let file_type = entry.file_type()?;
91
92            if file_type.is_dir() {
93                dirs.push(path);
94            } else if file_type.is_file() {
95                if path.extension().and_then(|s| s.to_str()) == Some("css") {
96                    css_files.push(path);
97                }
98            }
99        }
100    }
101
102    css_files.sort();
103    Ok(css_files)
104}
105
106/// Errors that can occur during CSS bundling.
107#[derive(Debug)]
108pub enum CssBundlerError {
109    /// Failed to read the source directory.
110    ReadSource { path: PathBuf, reason: String },
111    /// Failed to write the output file.
112    WriteOutput { path: PathBuf, reason: String },
113    /// No CSS files found in the source directory.
114    NoFilesFound(PathBuf),
115    /// CSS bundling/minification failed.
116    Bundle(String),
117}
118
119impl std::fmt::Display for CssBundlerError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            CssBundlerError::ReadSource { path, reason } => {
123                write!(f, "failed to read source dir {}: {}", path.display(), reason)
124            }
125            CssBundlerError::WriteOutput { path, reason } => {
126                write!(f, "failed to write output file {}: {}", path.display(), reason)
127            }
128            CssBundlerError::NoFilesFound(path) => {
129                write!(f, "no CSS files found in {}", path.display())
130            }
131            CssBundlerError::Bundle(msg) => {
132                write!(f, "CSS bundling failed: {}", msg)
133            }
134        }
135    }
136}
137
138impl std::error::Error for CssBundlerError {}
139