sabry_build/
buildmagic.rs1use std::{
2 collections::HashSet,
3 convert::Infallible,
4 fs::{self, OpenOptions},
5 io::{self, Write},
6 path::PathBuf,
7 str::FromStr,
8 vec,
9};
10
11use sabry_intrnl::{
12 compiler::{CompilerAdapter, SabryCompilerError},
13 config::{manifest::ManifestError, BehavHashCollision, BehavSassModCollision, SabryConfig},
14 scoper::{hash::ScopeHash, ArbitraryScope, ScopeError},
15};
16use sabry_procmacro_impl::impls::{styly, ArbitraryStyleSyntax};
17use walkdir::WalkDir;
18
19use crate::filevisit::{self, FileVisitError};
20
21type ModuleName = String;
22type ModuleCode = String;
23type StyleModule = (ModuleName, ModuleCode);
24type BuilderResult = Result<(), SabryBuildError>;
25
26pub fn buildy(inline_side_modules: impl IntoIterator<Item = StyleModule>) -> BuilderResult {
50 let config = SabryConfig::require()?;
51 let mut builder = SabryBuilder::new(config);
52 builder.build(inline_side_modules)?;
53
54 Ok(())
55}
56
57pub struct SabryBuilder {
69 config: SabryConfig,
70 css_compiler: CompilerAdapter,
71 state: SabryBuildState,
72}
73
74impl SabryBuilder {
76 pub fn new(config: SabryConfig) -> Self {
85 let css_compiler = CompilerAdapter::new(config.clone());
86 Self {
87 config,
88 css_compiler,
89 state: SabryBuildState::default(),
90 }
91 }
92
93 pub fn build(
95 &mut self,
96 inline_side_modules: impl IntoIterator<Item = StyleModule>,
97 ) -> BuilderResult {
98 println!("🧙: This is probably the stderr. Something went wrong:");
99
100 println!("🧙 loading preludes");
101 self.load_preludes()?;
102
103 println!("🧙 loading `buildy` modules");
104 for (name, code) in inline_side_modules {
105 self.load_side_module(name, code)?;
106 }
107
108 println!("🧙 loading this crate");
109 self.load_styles_from_this_crate()?;
110
111 println!("🧙 compiling CSS");
112 self.compile_everything()?;
113
114 println!("🧙 writing an output");
115 self.generate_output()?;
116
117 Ok(())
118 }
119
120 pub fn generate_output(&mut self) -> BuilderResult {
125 if self.state.loaded_css_modules.is_empty() {
127 println!("🧙 sabry didn't compile any CSS. Perhaps the crate has no styles? Lets write some!\nAlso you should check that you do use `styly!` macro properly - at the top level as an item.");
128 }
129
130 if let Some(scope_dir) = &self.config.css.scopes {
131 println!("🧙 writing CSS files for each of loaded scopes into {scope_dir}");
132
133 fs::create_dir_all(scope_dir)?;
134 for (scope, code) in &self.state.loaded_css_modules {
135 let scope_path = format!("{}/{}.css", scope_dir, scope);
136 fs::write(scope_path, code)?;
137 }
138 }
139
140 if let Some(bundle_file) = &self.config.css.bundle {
141 println!("🧙 writing merged CSS for the entire crate into {bundle_file}");
142
143 let path = PathBuf::from_str(bundle_file)?;
144
145 if let Some(dir) = path.parent() {
146 fs::create_dir_all(dir)?;
147 }
148
149 let _ = fs::remove_file(&path);
151 let mut file = OpenOptions::new()
153 .create_new(true)
154 .append(true)
155 .open(path)?;
156
157 let mut buffer = String::new();
159 for (_, code) in &self.state.loaded_css_modules {
160 buffer.push_str(code);
161 }
162 buffer = self.css_compiler.lightningcss(&buffer)?;
163
164 write!(file, "{buffer}")?;
165 }
166
167 Ok(())
168 }
169
170 pub fn compile_everything(&mut self) -> BuilderResult {
172 if self.state.known_side_modules.is_empty() {
174 println!("🧙 sabry didn't load any side modules");
175 }
176 if self.state.loaded_stylyses.is_empty() {
177 println!("🧙 sabry didn't load any usable styles");
178 }
179
180 for styly in &self.state.loaded_stylyses {
182 let scope = ArbitraryScope::from_source(
183 styly.syntax.into(),
184 styly.scope.clone(),
185 styly.code.code(),
186 )?
187 .hashed(&self.config.hash)?;
188
189 match self.config.hash.collision {
190 BehavHashCollision::Ignore => {}
191 BehavHashCollision::Error => {
192 if self.state.known_scope_hashes.contains(&scope.hash) {
193 return Err(SabryBuildError::HashCollision {
194 scope: scope.original_scope.name.to_string(),
195 });
196 }
197 self.state.known_scope_hashes.insert(scope.hash.clone());
198 }
199 }
200
201 let css = self
202 .css_compiler
203 .compile_module(scope.original_scope.adapter().syntax, &scope.hashed_code)?;
204 self.state
205 .loaded_css_modules
206 .push((scope.original_scope.name.to_string(), css));
207 }
208
209 for pre in &self.state.sass_prelude {
211 let css = self
212 .css_compiler
213 .compile_module(pre.syntax.into(), &pre.code)?;
214 self.state.css_prelude.push_str(&css);
215 }
216
217 Ok(())
218 }
219
220 pub fn load_styles_from_this_crate(&mut self) -> BuilderResult {
228 println!("🧙 scanning the crate");
229
230 let root = WalkDir::new(&self.config.sass.scanroot);
231
232 for entry in root {
233 let entry = entry?;
234 let metadata = entry.metadata()?;
235
236 if metadata.is_file() {
237 let entry_path = entry.path();
238 let ext = entry_path.extension().unwrap_or_default();
239 if ext == "rs" {
240 println!(".. reading {entry_path:?}");
241 let visitor = filevisit::visit_file(entry_path)?;
242 self.state.loaded_stylyses.extend(visitor.found_stylys);
243 }
244 } else if metadata.is_symlink() {
245 println!(
246 "🧙 sabry won't go through symlinks currently ('{}' is a symlink)",
247 entry.path().to_string_lossy()
248 )
249 }
250 }
251
252 Ok(())
253 }
254
255 pub fn load_preludes(&mut self) -> BuilderResult {
257 let mut modules: Vec<StyleModule> = vec![];
259 if let Some(sass_mods) = &self.config.sass.modules {
260 for pre in sass_mods {
261 let pre_path = PathBuf::from_str(pre)?;
262 let pre_name = pre_path
263 .file_name()
264 .ok_or(SabryBuildError::FileName())?
265 .to_string_lossy()
266 .to_string();
267 let code = fs::read_to_string(pre_path)?;
268 modules.push((pre_name, code));
269 }
270 }
271 for (name, code) in modules {
272 self.load_side_module(name, code)?;
273 }
274
275 if let Some(sass_pres) = &self.config.sass.prelude {
277 let mut sass_preludes: Vec<SassPreludeModule> = vec![];
278 for pre in sass_pres {
279 let pre_path = PathBuf::from_str(pre)?;
280
281 let syntax = pre_path
282 .extension()
283 .unwrap_or_default()
284 .to_str()
285 .unwrap_or_default();
286 let syntax = match ArbitraryStyleSyntax::try_from(syntax) {
287 Ok(s) => s,
288 Err(_) => {
289 return Err(SabryBuildError::Another(format!(
290 "Unknown syntax for sass prelude {pre}"
291 )))
292 }
293 };
294
295 let code = fs::read_to_string(pre_path)?;
296 sass_preludes.push(SassPreludeModule { syntax, code });
297 }
298 self.state.sass_prelude.extend(sass_preludes);
299 }
300
301 if let Some(css_pre) = &self.config.css.prelude {
303 for pre in css_pre {
304 let code = fs::read_to_string(pre)?;
305 self.state.css_prelude.push_str(&code);
306 }
307 }
308
309 Ok(())
310 }
311
312 pub fn load_side_module(&mut self, name: ModuleName, code: ModuleCode) -> BuilderResult {
322 println!("🧙 loading side module '{}'", &name);
323
324 let module_file_path = format!("{}/{}", &self.config.sass.intermediate_dir, &name);
325 let mut open_options = OpenOptions::new();
326 let mut open_options = open_options.write(true);
327
328 if !self.state.known_side_modules.contains(&name) {
330 let _ = fs::remove_file(&module_file_path);
331 }
332
333 open_options = if self.state.known_side_modules.contains(&name) {
334 match &self.config.sass.module_name_collision {
335 BehavSassModCollision::Error => {
336 return Err(SabryBuildError::ModuleCollision { module: name })
337 }
338 BehavSassModCollision::Merge => {
339 println!("🧙 sabry found duplicate module name '{name}': merging code, as configured");
340 open_options.append(true)
341 }
342 }
343 } else {
344 open_options.create(true)
345 };
346
347 fs::create_dir_all(&self.config.sass.intermediate_dir)?;
348 let mut module_file = open_options.open(module_file_path)?;
349
350 write!(module_file, "\n{code}\n")?;
351
352 self.state.known_side_modules.insert(name);
353
354 Ok(())
355 }
356}
357
358#[derive(Default)]
359pub struct SabryBuildState {
360 known_scope_hashes: HashSet<ScopeHash>,
363 known_side_modules: HashSet<ModuleName>,
366 loaded_stylyses: Vec<styly::MacroSyntax>,
368 loaded_css_modules: Vec<StyleModule>,
370 css_prelude: String,
373 sass_prelude: Vec<SassPreludeModule>,
376}
377
378pub struct SassPreludeModule {
380 syntax: ArbitraryStyleSyntax,
382 code: String,
384}
385
386#[derive(Debug, thiserror::Error)]
387pub enum SabryBuildError {
388 #[error("Filesystem error")]
389 Fs(#[from] io::Error),
390 #[error("Failed to walk through the crate")]
391 CrateWalk(#[from] walkdir::Error),
392 #[error("Failed to load side SASS module")]
393 LoadSass(),
394 #[error(
395 "Module with the similar name was already loaded, and sabry is configured to raise an error"
396 )]
397 ModuleCollision { module: ModuleName },
398 #[error("Sabry cant understand the file")]
399 FileVisit(#[from] FileVisitError),
400 #[error("Syntax of style can not be parsed")]
401 SyntaxError(#[from] ScopeError),
402 #[error("Another scope has the same hash, and sabry is configured to raise an error. Try to adjust config, increase hash size, or change the style code")]
403 HashCollision { scope: ModuleName },
404 #[error("Something's wrong with file path")]
405 Path(#[from] Infallible),
406 #[error("File name wasnt determined properly")]
407 FileName(),
408 #[error("Failed to compile CSS")]
409 CssCompile(#[from] SabryCompilerError),
410 #[error("Failed to load config/manifest")]
411 Manifest(#[from] ManifestError),
412 #[error("Another error")]
413 Another(String),
414}