initial os release. I really expect SO much bugs, guys. good luck =)
This commit is contained in:
commit
7d39af11b7
4 changed files with 7965 additions and 0 deletions
28
sabry_build/Cargo.toml
Normal file
28
sabry_build/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "sabry_build"
|
||||
description = "Sabry build magic as a separate crate"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
sabry_intrnl = {workspace = true}
|
||||
sabry_procmacro_impl = {workspace = true}
|
||||
proc-macro2 = {workspace = true, features = ["span-locations"]}
|
||||
syn = {workspace = true, features = ["visit", "derive", "parsing", "full"]}
|
||||
thiserror = {workspace = true}
|
||||
|
||||
walkdir = {version = "^2.5"}
|
||||
|
||||
[features]
|
||||
sass = [
|
||||
"sabry_procmacro_impl/sass",
|
||||
"sabry_intrnl/sass"
|
||||
]
|
||||
scss = [
|
||||
"sabry_procmacro_impl/scss",
|
||||
"sabry_intrnl/scss"
|
||||
]
|
||||
net-files = []
|
||||
305
sabry_build/src/buildmagic.rs
Normal file
305
sabry_build/src/buildmagic.rs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
convert::Infallible,
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
str::FromStr, vec,
|
||||
};
|
||||
|
||||
use sabry_intrnl::{
|
||||
compiler::{CompilerAdapter, SabryCompilerError},
|
||||
config::{
|
||||
manifest::ManifestError,
|
||||
BehavHashCollision, BehavSassModCollision, SabryConfig,
|
||||
},
|
||||
scoper::{hash::ScopeHash, ArbitraryScope, ScopeError},
|
||||
};
|
||||
use sabry_procmacro_impl::impls::styly;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::filevisit::{self, FileVisitError};
|
||||
|
||||
type ModuleName = String;
|
||||
type ModuleCode = String;
|
||||
type StyleModule = (ModuleName, ModuleCode);
|
||||
type BuilderResult = Result<(), SabryBuildError>;
|
||||
|
||||
pub fn buildy(inline_side_modules: impl IntoIterator<Item = StyleModule>) -> BuilderResult {
|
||||
println!("🧙: This is probably the stderr. Something went wrong:");
|
||||
|
||||
let config = SabryConfig::require()?;
|
||||
let mut builder = SabryBuilder::new(config);
|
||||
|
||||
println!("🧙 loading preludes");
|
||||
builder.load_preludes()?;
|
||||
|
||||
println!("🧙 loading `buildy` modules");
|
||||
for (name, code) in inline_side_modules {
|
||||
builder.load_side_module(name, code)?;
|
||||
}
|
||||
|
||||
println!("🧙 loading this crate");
|
||||
builder.load_styles_from_this_crate()?;
|
||||
|
||||
println!("🧙 compiling CSS");
|
||||
builder.compile_everything()?;
|
||||
|
||||
println!("🧙 writing an output");
|
||||
builder.generate_output()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct SabryBuilder {
|
||||
config: SabryConfig,
|
||||
css_compiler: CompilerAdapter,
|
||||
state: SabryBuildState,
|
||||
}
|
||||
|
||||
//🧙
|
||||
impl SabryBuilder {
|
||||
pub fn new(config: SabryConfig) -> Self {
|
||||
let css_compiler = CompilerAdapter::new(config.clone());
|
||||
Self {
|
||||
config,
|
||||
css_compiler,
|
||||
state: SabryBuildState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write all the loaded CSS
|
||||
///
|
||||
/// - Bundle file (if configured)
|
||||
/// - Scope chunks (if out-dir is configured)
|
||||
pub fn generate_output(&mut self) -> BuilderResult {
|
||||
// warn on empty loaded_css_modules
|
||||
if self.state.loaded_css_modules.is_empty() {
|
||||
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.");
|
||||
}
|
||||
|
||||
if let Some(scope_dir) = &self.config.css.scopes {
|
||||
println!("🧙 writing CSS files for each of loaded scopes into {scope_dir}");
|
||||
|
||||
fs::create_dir_all(scope_dir)?;
|
||||
for (scope, code) in &self.state.loaded_css_modules {
|
||||
let scope_path = format!("{}/{}.css", scope_dir, scope);
|
||||
fs::write(scope_path, code)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bundle_file) = &self.config.css.bundle {
|
||||
println!("🧙 writing merged CSS for the entire crate into {bundle_file}");
|
||||
|
||||
let path = PathBuf::from_str(bundle_file)?;
|
||||
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
|
||||
// remove the file to write new fresh tasty CSS
|
||||
let _ = fs::remove_file(&path);
|
||||
// and then reopen it
|
||||
let mut file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.append(true)
|
||||
.open(path)?;
|
||||
|
||||
for (_, code) in &self.state.loaded_css_modules {
|
||||
write!(file, " {}", self.state.css_prelude)?;
|
||||
write!(file, " {code}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile all the loaded styles, SASS/SCSS/CSS, without actually writing them.
|
||||
pub fn compile_everything(&mut self) -> BuilderResult {
|
||||
// warn on empty known_side_modules and loaded_stylyses
|
||||
if self.state.known_side_modules.is_empty() {
|
||||
println!("🧙 sabry didn't load any side modules");
|
||||
}
|
||||
if self.state.loaded_stylyses.is_empty() {
|
||||
println!("🧙 sabry didn't load any usable styles");
|
||||
}
|
||||
|
||||
for styly in &self.state.loaded_stylyses {
|
||||
let scope = ArbitraryScope::from_source(
|
||||
styly.syntax.into(),
|
||||
styly.scope.clone(),
|
||||
styly.code.code(),
|
||||
)?
|
||||
.hashed(&self.config.hash)?;
|
||||
|
||||
match self.config.hash.collision {
|
||||
BehavHashCollision::Ignore => {}
|
||||
BehavHashCollision::Error => {
|
||||
if self.state.known_scope_hashes.contains(&scope.hash) {
|
||||
return Err(SabryBuildError::HashCollision {
|
||||
scope: scope.original_scope.name.to_string(),
|
||||
});
|
||||
}
|
||||
self.state.known_scope_hashes.insert(scope.hash.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let css = self.css_compiler.compile_module(
|
||||
scope.original_scope.adapter().syntax.into(),
|
||||
&scope.hashed_code,
|
||||
)?;
|
||||
self.state
|
||||
.loaded_css_modules
|
||||
.push((scope.original_scope.name.to_string(), css));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Visit all the source files in the current crate and look for code that may affect building process:
|
||||
///
|
||||
/// - `styly!` macro calls
|
||||
///
|
||||
/// Only files with an **.rs** extension are visited.
|
||||
///
|
||||
/// All the gathered info is loaded into state
|
||||
pub fn load_styles_from_this_crate(&mut self) -> BuilderResult {
|
||||
println!("🧙 scanning the crate");
|
||||
|
||||
let root = WalkDir::new(&self.config.sass.scanroot);
|
||||
|
||||
for entry in root {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
|
||||
if metadata.is_file() {
|
||||
let entry_path = entry.path();
|
||||
let ext = entry_path.extension().unwrap_or_default();
|
||||
if ext == "rs" {
|
||||
let visitor = filevisit::visit_file(entry_path)?;
|
||||
self.state.loaded_stylyses.extend(visitor.found_stylys);
|
||||
}
|
||||
} else if metadata.is_symlink() {
|
||||
println!(
|
||||
"🧙 sabry won't go through symlinks currently ('{}' is a symlink)",
|
||||
entry.path().to_string_lossy()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load up configured preludes as side modules
|
||||
pub fn load_preludes(&mut self) -> BuilderResult {
|
||||
// load sass modules from config
|
||||
let mut modules: Vec<StyleModule> = vec![];
|
||||
if let Some(sass_pre) = &self.config.sass.modules {
|
||||
for pre in sass_pre {
|
||||
let pre_path = PathBuf::from_str(pre)?;
|
||||
let pre_name = pre_path
|
||||
.file_name()
|
||||
.ok_or(SabryBuildError::FileName())?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let code = fs::read_to_string(pre_path)?;
|
||||
modules.push((pre_name, code));
|
||||
}
|
||||
}
|
||||
for (name, code) in modules {
|
||||
self.load_side_module(name, code)?;
|
||||
}
|
||||
|
||||
// load css preludes
|
||||
if let Some(css_pre) = &self.config.css.bundle_prelude {
|
||||
for pre in css_pre {
|
||||
let code = fs::read_to_string(pre)?;
|
||||
self.state.css_prelude.push_str(&code);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load up the SASS module by its `name` and fill the according file by its `code` as-is
|
||||
///
|
||||
/// Module "loading" is simply a file creation. File is created in the configured `intermediate_dir`
|
||||
/// directory.
|
||||
///
|
||||
/// Module `name` is not suffixed with syntax/extensions/whatever so it already should have necessary file extensions.
|
||||
///
|
||||
/// This function will not overwrite the module file. Never.
|
||||
///
|
||||
pub fn load_side_module(&mut self, name: ModuleName, code: ModuleCode) -> BuilderResult {
|
||||
println!("🧙 loading side module '{}'", &name);
|
||||
|
||||
let module_file_path = format!("{}/{}", &self.config.sass.intermediate_dir, &name);
|
||||
let mut open_options = OpenOptions::new();
|
||||
let mut open_options = open_options.write(true);
|
||||
|
||||
// prematurelly remove module file if the module is not yet known to be loaded
|
||||
if !self.state.known_side_modules.contains(&name) {
|
||||
let _ = fs::remove_file(&module_file_path);
|
||||
}
|
||||
|
||||
open_options = if self.state.known_side_modules.contains(&name) {
|
||||
match &self.config.sass.module_name_collision {
|
||||
BehavSassModCollision::Error => {
|
||||
return Err(SabryBuildError::ModuleCollision { module: name })
|
||||
}
|
||||
BehavSassModCollision::Merge => {
|
||||
println!("🧙 sabry found duplicate module name '{name}': merging code, as configured");
|
||||
open_options.append(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
open_options.create(true)
|
||||
};
|
||||
|
||||
fs::create_dir_all(&self.config.sass.intermediate_dir)?;
|
||||
let mut module_file = open_options.open(module_file_path)?;
|
||||
|
||||
write!(module_file, "\n{code}\n")?;
|
||||
|
||||
self.state.known_side_modules.insert(name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SabryBuildState {
|
||||
known_scope_hashes: HashSet<ScopeHash>,
|
||||
known_side_modules: HashSet<ModuleName>,
|
||||
loaded_stylyses: Vec<styly::MacroSyntax>,
|
||||
loaded_css_modules: Vec<StyleModule>,
|
||||
css_prelude: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SabryBuildError {
|
||||
#[error("Filesystem error")]
|
||||
Fs(#[from] io::Error),
|
||||
#[error("Failed to walk through the crate")]
|
||||
CrateWalk(#[from] walkdir::Error),
|
||||
#[error("Failed to load side SASS module")]
|
||||
LoadSass(),
|
||||
#[error(
|
||||
"Module with the similar name was already loaded, and sabry is configured to raise an error"
|
||||
)]
|
||||
ModuleCollision { module: ModuleName },
|
||||
#[error("Sabry cant understand the file")]
|
||||
FileVisit(#[from] FileVisitError),
|
||||
#[error("Syntax of style can not be parsed")]
|
||||
SyntaxParse(#[from] ScopeError),
|
||||
#[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")]
|
||||
HashCollision { scope: ModuleName },
|
||||
#[error("Something's wrong with file path")]
|
||||
Path(#[from] Infallible),
|
||||
#[error("File name wasnt determined properly")]
|
||||
FileName(),
|
||||
#[error("Failed to compile CSS")]
|
||||
CssCompile(#[from] SabryCompilerError),
|
||||
#[error("Failed to load config/manifest")]
|
||||
Manifest(#[from] ManifestError),
|
||||
}
|
||||
51
sabry_build/src/filevisit.rs
Normal file
51
sabry_build/src/filevisit.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use std::{fs, io, path::Path};
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use sabry_procmacro_impl::impls::styly;
|
||||
use syn::{spanned::Spanned, visit::Visit};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FileVisitError {
|
||||
#[error("Could not read file")]
|
||||
Read(#[from] io::Error),
|
||||
#[error("styly! macro found, yet invalid")]
|
||||
Styly(#[from] syn::Error),
|
||||
}
|
||||
|
||||
/// Visit file, returning either [StylyVisitor] or error
|
||||
pub fn visit_file(path: &Path) -> Result<StylyVisitor, FileVisitError> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let code_file = syn::parse_file(&content)?;
|
||||
|
||||
let mut styly_visitor = StylyVisitor {
|
||||
found_stylys: vec![],
|
||||
};
|
||||
styly_visitor.visit_file(&code_file);
|
||||
|
||||
Ok(styly_visitor)
|
||||
}
|
||||
|
||||
/// Syn Visitor, which will look at every `styly!` macro,
|
||||
/// parse its input tokenstream and avaluate it, capturing [styly::MacroSyntax] result
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// If either macro body is not a [TokenStream], or [styly::parse_macro_syntax] is not [Ok]
|
||||
#[derive(Debug)]
|
||||
pub struct StylyVisitor {
|
||||
pub found_stylys: Vec<styly::MacroSyntax>,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for StylyVisitor {
|
||||
fn visit_item_macro(&mut self, node: &'ast syn::ItemMacro) {
|
||||
if node.mac.path.get_ident().is_some_and(|i| i == "styly") {
|
||||
let body = node.mac.parse_body::<TokenStream>().unwrap_or_else(|_| {
|
||||
panic!("could not parse `styly!` macro at {:?}", node.mac.span())
|
||||
});
|
||||
let macro_data = styly::parse_macro_syntax(body).unwrap_or_else(|_| {
|
||||
panic!("could not parse `styly!` macro at {:?}", node.mac.span())
|
||||
});
|
||||
self.found_stylys.push(macro_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
sabry_build/src/lib.rs
Normal file
3
sabry_build/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod buildmagic;
|
||||
pub mod filevisit;
|
||||
pub use buildmagic::*;
|
||||
Loading…
Reference in a new issue