mini_static/
css_bundler.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::bundle;
5use crate::minify;
6
7pub 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
32pub 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
98fn 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(¤t_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() && path.extension().and_then(|s| s.to_str()) == Some("css")
113 {
114 css_files.push(path);
115 }
116 }
117 }
118
119 css_files.sort();
120 Ok(css_files)
121}
122
123#[derive(Debug)]
125pub enum CssBundlerError {
126 ReadSource { path: PathBuf, reason: String },
128 WriteOutput { path: PathBuf, reason: String },
130 NoFilesFound(PathBuf),
132 Bundle(String),
134}
135
136impl std::fmt::Display for CssBundlerError {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 CssBundlerError::ReadSource { path, reason } => {
140 write!(
141 f,
142 "failed to read source dir {}: {}",
143 path.display(),
144 reason
145 )
146 }
147 CssBundlerError::WriteOutput { path, reason } => {
148 write!(
149 f,
150 "failed to write output file {}: {}",
151 path.display(),
152 reason
153 )
154 }
155 CssBundlerError::NoFilesFound(path) => {
156 write!(f, "no CSS files found in {}", path.display())
157 }
158 CssBundlerError::Bundle(msg) => {
159 write!(f, "CSS bundling failed: {}", msg)
160 }
161 }
162 }
163}
164
165impl std::error::Error for CssBundlerError {}