Skip to main content

mini_static/
css_bundler.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::bundle;
5use crate::minify;
6
7/// Bundle all CSS files from a single source directory into a single output file.
8///
9/// Convenience wrapper around [`bundle_css_sources`] for callers with one source directory.
10///
11/// # Errors
12///
13/// See [`bundle_css_sources`].
14pub async fn bundle_directory_css(
15    src_dir: &Path,
16    output_path: &Path,
17) -> Result<(), CssBundlerError> {
18    let src_dir_canon = src_dir
19        .canonicalize()
20        .map_err(|e| CssBundlerError::ReadSource {
21            path: src_dir.to_path_buf(),
22            reason: e.to_string(),
23        })?;
24    bundle_css_sources(
25        std::slice::from_ref(&src_dir_canon),
26        std::slice::from_ref(&src_dir_canon),
27        output_path,
28    )
29    .await
30}
31
32/// Bundle all CSS files across `source_dirs` into a single output file.
33///
34/// Discovers every `.css` file under `source_dirs` (recursively), resolves `@import`
35/// statements within `allowed_roots`, concatenates the per-file bundles in sorted path
36/// order, minifies the result, and writes it to `output_path`. `allowed_roots` should
37/// include `source_dirs` plus any files-only import roots (see [`Server::with_bundle_root`]).
38///
39/// # Errors
40///
41/// Returns `Err` if:
42/// - any of `source_dirs` cannot be read
43/// - no `.css` files exist under `source_dirs`
44/// - `output_path` cannot be written to
45/// - CSS parsing or bundling fails
46pub async fn bundle_css_sources(
47    allowed_roots: &[PathBuf],
48    source_dirs: &[PathBuf],
49    output_path: &Path,
50) -> Result<(), CssBundlerError> {
51    let mut css_files = Vec::new();
52
53    for src_dir in source_dirs {
54        let files = find_css_files(src_dir).map_err(|e| CssBundlerError::ReadSource {
55            path: src_dir.to_path_buf(),
56            reason: e.to_string(),
57        })?;
58        css_files.extend(files);
59    }
60
61    if css_files.is_empty() {
62        let first = source_dirs
63            .first()
64            .cloned()
65            .unwrap_or_else(|| PathBuf::from("."));
66        return Err(CssBundlerError::NoFilesFound(first));
67    }
68
69    let mut bundled_content = String::new();
70
71    for css_file in &css_files {
72        let (bytes, _deps) = bundle::bundle_and_minify_css(allowed_roots, css_file)
73            .await
74            .map_err(|e| CssBundlerError::Bundle(format!("{:?}", e)))?;
75
76        bundled_content.push_str(&String::from_utf8_lossy(&bytes));
77    }
78
79    let bundled_bytes_final =
80        minify::minify(bundled_content.as_bytes(), crate::reload::ChangeType::Css)
81            .map_err(|e| CssBundlerError::Bundle(format!("Minification failed: {:?}", e)))?;
82
83    if let Some(parent) = output_path.parent() {
84        fs::create_dir_all(parent).map_err(|e| CssBundlerError::WriteOutput {
85            path: output_path.to_path_buf(),
86            reason: e.to_string(),
87        })?;
88    }
89
90    fs::write(output_path, &bundled_bytes_final).map_err(|e| CssBundlerError::WriteOutput {
91        path: output_path.to_path_buf(),
92        reason: e.to_string(),
93    })?;
94
95    Ok(())
96}
97
98/// Find all `.css` files in a directory tree.
99fn find_css_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
100    let mut css_files = Vec::new();
101    let mut dirs = vec![dir.to_path_buf()];
102
103    while let Some(current_dir) = dirs.pop() {
104        let entries = fs::read_dir(&current_dir)?;
105        for entry in entries {
106            let entry = entry?;
107            let path = entry.path();
108            let file_type = entry.file_type()?;
109
110            if file_type.is_dir() {
111                dirs.push(path);
112            } else if file_type.is_file()
113                && path.extension().and_then(|s| s.to_str()) == Some("css")
114            {
115                css_files.push(path);
116            }
117        }
118    }
119
120    css_files.sort();
121    Ok(css_files)
122}
123
124/// Errors that can occur during CSS bundling.
125#[derive(Debug)]
126pub enum CssBundlerError {
127    /// Failed to read the source directory.
128    ReadSource { path: PathBuf, reason: String },
129    /// Failed to write the output file.
130    WriteOutput { path: PathBuf, reason: String },
131    /// No CSS files found in the source directory.
132    NoFilesFound(PathBuf),
133    /// CSS bundling/minification failed.
134    Bundle(String),
135}
136
137impl std::fmt::Display for CssBundlerError {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        match self {
140            CssBundlerError::ReadSource { path, reason } => {
141                write!(
142                    f,
143                    "failed to read source dir {}: {}",
144                    path.display(),
145                    reason
146                )
147            }
148            CssBundlerError::WriteOutput { path, reason } => {
149                write!(
150                    f,
151                    "failed to write output file {}: {}",
152                    path.display(),
153                    reason
154                )
155            }
156            CssBundlerError::NoFilesFound(path) => {
157                write!(f, "no CSS files found in {}", path.display())
158            }
159            CssBundlerError::Bundle(msg) => {
160                write!(f, "CSS bundling failed: {}", msg)
161            }
162        }
163    }
164}
165
166impl std::error::Error for CssBundlerError {}