,
35 | }
36 |
--------------------------------------------------------------------------------
/turf_internals/src/path_utils.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 |
3 | #[derive(thiserror::Error, Debug)]
4 | #[error("error resolving path '{path}' - {source}")]
5 | pub struct PathResolutionError {
6 | pub(crate) path: PathBuf,
7 | pub(crate) source: std::io::Error,
8 | }
9 |
10 | impl From<(PathBuf, std::io::Error)> for PathResolutionError {
11 | fn from(value: (PathBuf, std::io::Error)) -> Self {
12 | Self {
13 | path: value.0,
14 | source: value.1,
15 | }
16 | }
17 | }
18 |
19 | pub fn canonicalize(path: P) -> Result
20 | where
21 | P: AsRef,
22 | {
23 | let mut canonicalized_path = PathBuf::from(
24 | std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR environment variable"),
25 | );
26 | canonicalized_path.push(path.as_ref());
27 |
28 | std::fs::canonicalize(canonicalized_path.clone()).map_err(|e| (canonicalized_path, e).into())
29 | }
30 |
31 | pub fn get_file_paths_recusively(path: PathBuf) -> Result, PathResolutionError> {
32 | use std::fs::read_dir;
33 |
34 | let path = canonicalize(path)?;
35 | let mut result = Vec::new();
36 |
37 | for item in read_dir(path.clone()).map_err(|e| (path.clone(), e))? {
38 | let item_path = item.map_err(|e| (path.clone(), e))?.path();
39 |
40 | if item_path.is_file() {
41 | result.push(canonicalize(item_path)?);
42 | } else if item_path.is_dir() {
43 | result.extend(get_file_paths_recusively(item_path)?);
44 | }
45 | }
46 |
47 | Ok(result)
48 | }
49 |
--------------------------------------------------------------------------------
/turf_internals/src/css_compilation.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 |
3 | use crate::{path_utils, Settings, StyleSheetKind};
4 |
5 | #[derive(thiserror::Error, Debug)]
6 | pub enum CssCompilationError {
7 | #[error("error compiling scss file '{1}' - {0}")]
8 | File(Box, PathBuf),
9 | #[error("error compiling inline scss '{0}'")]
10 | Inline(#[from] Box),
11 | #[error(transparent)]
12 | PathResolutionError(#[from] path_utils::PathResolutionError),
13 | }
14 |
15 | impl From<(Box, P)> for CssCompilationError
16 | where
17 | P: AsRef + std::fmt::Debug,
18 | {
19 | fn from(value: (Box, P)) -> Self {
20 | let canonicalized_path = value.1.as_ref().canonicalize();
21 |
22 | match canonicalized_path {
23 | Ok(path) => CssCompilationError::File(value.0, path),
24 | Err(e) => path_utils::PathResolutionError {
25 | path: value.1.as_ref().to_path_buf(),
26 | source: e,
27 | }
28 | .into(),
29 | }
30 | }
31 | }
32 |
33 | pub fn compile_style_sheet(
34 | style_sheet: &StyleSheetKind,
35 | settings: &Settings,
36 | ) -> Result {
37 | Ok(match style_sheet {
38 | StyleSheetKind::File(ref path) => grass::from_path(path, &settings.clone().try_into()?)
39 | .map_err(|e| CssCompilationError::from((e, path.clone())))?,
40 | StyleSheetKind::Inline(ref style_sheet) => {
41 | grass::from_string(style_sheet, &settings.clone().try_into()?)?
42 | }
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/turf_internals/src/file_output.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::{create_dir_all, File},
3 | io::Write,
4 | path::PathBuf,
5 | };
6 |
7 | use crate::{settings::FileOutput, StyleSheetKind};
8 |
9 | static DIRS_RESET: std::sync::OnceLock<()> = std::sync::OnceLock::new();
10 |
11 | #[derive(Debug, thiserror::Error)]
12 | #[error("error writing css file '{0}' - {1}")]
13 | pub struct CssFileWriteError(PathBuf, std::io::Error);
14 |
15 | fn reset_file_output(output_paths: &FileOutput) -> Result<(), CssFileWriteError> {
16 | if let Some(path) = &output_paths.global_css_file_path {
17 | if let Err(error) = std::fs::remove_file(path) {
18 | match error.kind() {
19 | std::io::ErrorKind::NotFound => {}
20 | _ => Err(CssFileWriteError(path.clone(), error))?,
21 | }
22 | };
23 |
24 | create_dir_all(path.parent().expect("global css file path has parent dir"))
25 | .map_err(|error| CssFileWriteError(path.clone(), error))?;
26 | }
27 | if let Some(path) = &output_paths.separate_css_files_path {
28 | if let Err(error) = std::fs::remove_dir_all(path) {
29 | match error.kind() {
30 | std::io::ErrorKind::NotFound => {}
31 | _ => Err(CssFileWriteError(path.clone(), error))?,
32 | }
33 | };
34 |
35 | create_dir_all(path).map_err(|error| CssFileWriteError(path.clone(), error))?;
36 | }
37 |
38 | Ok(())
39 | }
40 |
41 | fn append_to_separate_file(
42 | style: &str,
43 | mut separate_files_dir: PathBuf,
44 | style_sheet: &StyleSheetKind,
45 | ) -> Result<(), CssFileWriteError> {
46 | match style_sheet {
47 | StyleSheetKind::File(path) => {
48 | separate_files_dir.push(path.file_name().expect("current css file exists"));
49 | separate_files_dir.set_extension("css");
50 | }
51 | StyleSheetKind::Inline(style_sheet) => {
52 | let hash = xxhash_rust::xxh3::xxh3_64(style_sheet.as_bytes());
53 | separate_files_dir.push(&format!("{hash:x?}.css"));
54 | }
55 | };
56 |
57 | let mut output_file = File::options()
58 | .create(true)
59 | .append(true)
60 | .open(&separate_files_dir)
61 | .map_err(|error| CssFileWriteError(separate_files_dir.clone(), error))?;
62 |
63 | output_file
64 | .write_all(style.as_bytes())
65 | .map_err(|error| CssFileWriteError(separate_files_dir, error))?;
66 |
67 | Ok(())
68 | }
69 |
70 | fn append_to_global_file(style: &str, global_file_path: &PathBuf) -> Result<(), CssFileWriteError> {
71 | let mut global_css_file = File::options()
72 | .create(true)
73 | .append(true)
74 | .open(global_file_path)
75 | .map_err(|error| CssFileWriteError(global_file_path.clone(), error))?;
76 |
77 | global_css_file
78 | .write_all(style.as_bytes())
79 | .map_err(|error| CssFileWriteError(global_file_path.clone(), error))?;
80 |
81 | Ok(())
82 | }
83 |
84 | pub fn perform_css_file_output(
85 | output_paths: FileOutput,
86 | style: &str,
87 | style_sheet_kind: &StyleSheetKind,
88 | ) -> Result<(), CssFileWriteError> {
89 | if DIRS_RESET.get().is_none() {
90 | reset_file_output(&output_paths)?;
91 |
92 | DIRS_RESET
93 | .set(())
94 | .expect("internal turf state has already been set, but should be empty");
95 | }
96 |
97 | if let Some(output_path) = output_paths.separate_css_files_path {
98 | append_to_separate_file(style, output_path, style_sheet_kind)?;
99 | }
100 |
101 | if let Some(output_path) = output_paths.global_css_file_path {
102 | append_to_global_file(style, &output_path)?;
103 | }
104 |
105 | Ok(())
106 | }
107 |
--------------------------------------------------------------------------------
/turf_internals/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! You're probably looking for `turf` instead.
2 |
3 | mod css_compilation;
4 | mod file_output;
5 | mod hashing;
6 | mod manifest;
7 | mod path_utils;
8 | mod settings;
9 | mod transformer;
10 |
11 | use std::{collections::HashMap, path::PathBuf, sync::Mutex};
12 |
13 | pub use settings::Settings;
14 |
15 | #[derive(thiserror::Error, Debug)]
16 | pub enum Error {
17 | #[error(transparent)]
18 | CssCompilation(#[from] css_compilation::CssCompilationError),
19 | #[error(transparent)]
20 | Hashing(#[from] hashing::StyleSheetHashingError),
21 | #[error("error transforming css - {0}")]
22 | CssTransformation(#[from] transformer::TransformationError),
23 | #[error("no input file was specified")]
24 | NoInputFile,
25 | #[error(transparent)]
26 | PathResolution(#[from] path_utils::PathResolutionError),
27 |
28 | #[error(transparent)]
29 | CssFileWrite(#[from] file_output::CssFileWriteError),
30 | #[error(transparent)]
31 | Settings(#[from] settings::SettingsError),
32 | }
33 |
34 | fn compile_message(message: &str) {
35 | println!("🌱 turf [INFO]: {message}");
36 | }
37 |
38 | #[derive(Debug)]
39 | pub enum StyleSheetKind {
40 | File(PathBuf),
41 | Inline(String),
42 | }
43 |
44 | #[derive(Debug)]
45 | pub struct CompiledStyleSheet {
46 | pub css: String,
47 | pub class_names: HashMap,
48 | pub original_style_sheet: StyleSheetKind,
49 | }
50 |
51 | fn style_sheet_with_compile_options(
52 | style_sheet_input: StyleSheetKind,
53 | settings: Settings,
54 | ) -> Result {
55 | let hash = hashing::hash_style_sheet(&style_sheet_input)?;
56 | let css = css_compilation::compile_style_sheet(&style_sheet_input, &settings)?;
57 |
58 | let (style_sheet_css, class_names) =
59 | transformer::transform_stylesheet(&css, &hash, settings.clone())?;
60 |
61 | if let Some(file_output) = settings.file_output {
62 | file_output::perform_css_file_output(file_output, &style_sheet_css, &style_sheet_input)?;
63 | }
64 |
65 | Ok(CompiledStyleSheet {
66 | css: style_sheet_css,
67 | class_names,
68 | original_style_sheet: style_sheet_input,
69 | })
70 | }
71 |
72 | pub fn style_sheet(style_sheet: StyleSheetKind) -> Result {
73 | let settings = Settings::get()?;
74 |
75 | let style_sheet = match style_sheet {
76 | StyleSheetKind::File(path) => {
77 | if path == PathBuf::from("") {
78 | return Err(crate::Error::NoInputFile);
79 | };
80 | let canonicalized_path = path_utils::canonicalize(path)?;
81 | StyleSheetKind::File(canonicalized_path)
82 | }
83 | StyleSheetKind::Inline(inline_style_sheet) => StyleSheetKind::Inline(inline_style_sheet),
84 | };
85 |
86 | style_sheet_with_compile_options(style_sheet, settings)
87 | }
88 |
89 | static LOAD_PATHS_TRACKED: Mutex = Mutex::new(false);
90 |
91 | #[derive(Debug, thiserror::Error)]
92 | pub enum LoadPathTrackingError {
93 | #[error("Could not read internal state")]
94 | Mutex,
95 | #[error(transparent)]
96 | Settings(#[from] settings::SettingsError),
97 | #[error(transparent)]
98 | PathResolution(#[from] path_utils::PathResolutionError),
99 | }
100 |
101 | pub fn get_untracked_load_paths() -> Result, LoadPathTrackingError> {
102 | let mut load_paths_tracked = match LOAD_PATHS_TRACKED.lock() {
103 | Err(_) => return Err(LoadPathTrackingError::Mutex),
104 | Ok(val) => val,
105 | };
106 |
107 | if *load_paths_tracked {
108 | Ok(Vec::new())
109 | } else {
110 | let settings = Settings::get()?;
111 | *load_paths_tracked = true;
112 |
113 | let mut result = Vec::new();
114 |
115 | for path in settings.load_paths {
116 | result.extend(path_utils::get_file_paths_recusively(path)?);
117 | }
118 |
119 | Ok(result)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.10.1
2 |
3 | - Changed default class name template to a deterministic hash (thank you @TimTom2016 for creating a pr)
4 |
5 | # 0.10.0
6 |
7 | - Replaced object-based browser targets configuration with browserslist format
8 |
9 | # 0.9.6
10 |
11 | - Fixed vendor prefixing
12 |
13 | # 0.9.5
14 |
15 | - Fixed build on windows
16 |
17 | # 0.9.4
18 |
19 | - Add ``, ``, ``, and `` placeholders into `package.metadata.turf.class_names.template` configuration option (thank you @lukidoescode for creating a pr)
20 |
21 | # 0.9.3
22 |
23 | - Updated lightningcss
24 |
25 | # 0.9.2
26 |
27 | - Fixed settings parsing of browser version target arrays with a single major version specification (thank you @emnul for reporting the issue)
28 |
29 | # 0.9.1
30 |
31 | - Added error source information to error messages
32 |
33 | # 0.9.0
34 |
35 | - Updated dependencies
36 | - Renamed the `inline_style_sheet` macro to `style_sheet_values` to avoid confusion
37 | - Added the `inline_style_sheet` and `inline_style_sheet_values` macros to allow inline SCSS style definitions
38 | - Changed `randomized_class_name` to use base64 ids rather than numeric ids for randomizing class ids (thank you @BToersche)
39 | - Added support for `any`, `has`, `host`, `is`, `not`, `slotted` and `where` CSS pseudo classes. (thank you @BToersche)
40 |
41 | # 0.8.0
42 |
43 | - Updated dependencies
44 | - Removed `once_cell` feature flag
45 | - Removed support for Rust 1.65
46 |
47 | # 0.7.1
48 | - Fixed compilation on minimum supported Rust version by pinning dependency versions
49 | - Restructured project to allow specifying dependency from git repo
50 |
51 | # 0.7.0
52 | - Added optional configurable file output of the resulting CSS
53 | - Added the alternative `inline_style_sheet` macro which directly returns the CSS style sheet and a class names struct
54 | - The class name configuration is now located under the `class_names` key
55 | - Added the `excludes` configuration option for excluding class names from the uniquification process using regex
56 | - The minimum supported Rust version has been bumped to 1.65.0
57 |
58 | # 0.6.2
59 | - Fixed failing builds due to a badly specified dependency in one of turf's dependencies (thank you @xeho91 for offering a quick fix)
60 | - Updated lightningcss
61 |
62 | # 0.6.1
63 | - Fixed an error with the new path resolution which resulted in incorrect paths being used for the file tracking
64 |
65 | # 0.6.0
66 | - Added tracking of style sheets and files in `load_paths` (SCSS recompilation on file changes)
67 | - `load_paths` are now relative to the project directory they are specified in when using workspaces
68 |
69 | # 0.5.0
70 | - Updated grass to `0.13` (see [here](https://github.com/connorskees/grass/blob/master/CHANGELOG.md))
71 | - Added instructions to trigger recompilation on SCSS style changes
72 | - `ClassName` is now `pub` (thank you @xeho91 for creating a pull request)
73 | - `STYLE_SHEET` is now `pub`
74 |
75 | # 0.4.1
76 | - Improved messages of errors with the SCCS input file path
77 | - Fixed a misleading file path in error messages when using Cargo workspaces
78 |
79 | # 0.4.0
80 | - Minimum supported Rust version is now 1.70.0
81 | - New `once_cell` feature flag for backward compatibility down to Rust version 1.64.0
82 | - Added `[package.metadata.turf-dev]` profile for separate development and production build settings
83 | - The configuration is now cached to avoid reading it repeatedly from the config for every macro invocation
84 | - Added a `debug` configuration setting for debug output
85 | - Improved the SCSS compilation error message by providing the file path to the SCSS file that caused the error
86 |
87 | # 0.3.2
88 | - pinned version of `lightningcss` and `lightningcss-derive` to prevent incompatible releases from being used
89 |
90 | # 0.3.1
91 | - fixed an issue that resulted in a compile error (thank you @xeho91 for reporting the issue and creating a pr!) #1 #2
92 |
93 | # 0.3.0
94 | - [lightningcss](https://github.com/parcel-bundler/lightningcss) integration for minifying and optimizing CSS
95 | - configurable class name generation with unique and dynamic names
96 | - support for specifying browser targets and versions for CSS compatibility
97 | - improved documentation and examples
98 |
99 | # 0.2.1
100 | - Updated description / README
101 |
102 | # 0.2.0
103 | - removed configured_style_sheet
104 | - the style_sheet macro now reads the configuration from Cargo.toml like configured_style_sheet did and uses default settings as fallback
105 |
106 | # 0.1.1
107 |
108 | # 0.1.0
109 |
--------------------------------------------------------------------------------
/turf_macros/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! You're probably looking for `turf` instead.
2 |
3 | use convert_case::{Case, Casing};
4 | use std::{collections::HashMap, path::PathBuf};
5 | use turf_internals::{CompiledStyleSheet, StyleSheetKind};
6 |
7 | use proc_macro::TokenStream;
8 | use quote::quote;
9 |
10 | #[proc_macro]
11 | pub fn style_sheet(input: TokenStream) -> TokenStream {
12 | let input = input.to_string();
13 | let sanitized_path = PathBuf::from(input.trim_matches('"'));
14 |
15 | let ProcessedStyleSheet {
16 | untracked_load_paths,
17 | css,
18 | class_names,
19 | } = match handle_style_sheet(StyleSheetKind::File(sanitized_path)) {
20 | Ok(result) => result,
21 | Err(e) => {
22 | return match e {
23 | Error::Turf(e) => to_compile_error(e),
24 | Error::LoadPathTracking(e) => to_compile_error(e),
25 | }
26 | }
27 | };
28 |
29 | let mut out = quote! {
30 | pub static STYLE_SHEET: &'static str = #css;
31 | };
32 | out.extend(create_classes_structure(class_names));
33 | out.extend(create_include_bytes(untracked_load_paths));
34 |
35 | out.into()
36 | }
37 |
38 | #[proc_macro]
39 | pub fn style_sheet_values(input: TokenStream) -> TokenStream {
40 | let input = input.to_string();
41 | let sanitized_path = PathBuf::from(input.trim_matches('"'));
42 |
43 | let ProcessedStyleSheet {
44 | untracked_load_paths,
45 | css,
46 | class_names,
47 | } = match handle_style_sheet(StyleSheetKind::File(sanitized_path)) {
48 | Ok(result) => result,
49 | Err(e) => {
50 | return match e {
51 | Error::Turf(e) => to_compile_error(e),
52 | Error::LoadPathTracking(e) => to_compile_error(e),
53 | }
54 | }
55 | };
56 |
57 | let includes = create_include_bytes(untracked_load_paths);
58 | let inlines = create_inline_classes_instance(class_names);
59 | let out = quote! {{
60 | pub static STYLE_SHEET: &'static str = #css;
61 | #includes
62 | #inlines
63 | }};
64 |
65 | out.into()
66 | }
67 |
68 | #[proc_macro]
69 | pub fn inline_style_sheet(input: TokenStream) -> TokenStream {
70 | let input = input.to_string();
71 |
72 | let ProcessedStyleSheet {
73 | untracked_load_paths,
74 | css,
75 | class_names,
76 | } = match handle_style_sheet(StyleSheetKind::Inline(input)) {
77 | Ok(result) => result,
78 | Err(e) => {
79 | return match e {
80 | Error::Turf(e) => to_compile_error(e),
81 | Error::LoadPathTracking(e) => to_compile_error(e),
82 | }
83 | }
84 | };
85 |
86 | let mut out = quote! {
87 | pub static STYLE_SHEET: &'static str = #css;
88 | };
89 | out.extend(create_classes_structure(class_names));
90 | out.extend(create_include_bytes(untracked_load_paths));
91 |
92 | out.into()
93 | }
94 |
95 | #[proc_macro]
96 | pub fn inline_style_sheet_values(input: TokenStream) -> TokenStream {
97 | let input = input.to_string();
98 |
99 | let ProcessedStyleSheet {
100 | untracked_load_paths,
101 | css,
102 | class_names,
103 | } = match handle_style_sheet(StyleSheetKind::Inline(input)) {
104 | Ok(result) => result,
105 | Err(e) => {
106 | return match e {
107 | Error::Turf(e) => to_compile_error(e),
108 | Error::LoadPathTracking(e) => to_compile_error(e),
109 | }
110 | }
111 | };
112 |
113 | let includes = create_include_bytes(untracked_load_paths);
114 | let inlines = create_inline_classes_instance(class_names);
115 | let out = quote! {{
116 | pub static STYLE_SHEET: &'static str = #css;
117 | #includes
118 | #inlines
119 | }};
120 |
121 | out.into()
122 | }
123 |
124 | fn to_compile_error(e: E) -> TokenStream
125 | where
126 | E: std::error::Error,
127 | {
128 | let mut message = format!("Error: {}", e);
129 | let mut curr_err = e.source();
130 |
131 | if curr_err.is_some() {
132 | message.push_str("\nCaused by:");
133 | }
134 |
135 | while let Some(current_error) = curr_err {
136 | message.push_str(&format!("\n {}", current_error));
137 | curr_err = current_error.source();
138 | }
139 |
140 | quote! {
141 | compile_error!(#message);
142 | }
143 | .into()
144 | }
145 |
146 | fn create_classes_structure(classes: HashMap) -> proc_macro2::TokenStream {
147 | let original_class_names: Vec = classes
148 | .keys()
149 | .map(|class| class.to_case(Case::ScreamingSnake))
150 | .map(|class| quote::format_ident!("{}", class.as_str().to_uppercase()))
151 | .collect();
152 |
153 | let randomized_class_names: Vec<&String> = classes.values().collect();
154 |
155 | let doc = original_class_names
156 | .iter()
157 | .zip(randomized_class_names.iter())
158 | .fold(String::new(), |mut doc, (variable, class_name)| {
159 | doc.push_str(&format!("{} = \"{}\"\n", variable, class_name));
160 | doc
161 | });
162 |
163 | quote::quote! {
164 | #[doc=#doc]
165 | pub struct ClassName;
166 | impl ClassName {
167 | #(pub const #original_class_names: &'static str = #randomized_class_names;)*
168 | }
169 | }
170 | }
171 |
172 | fn create_inline_classes_instance(classes: HashMap) -> proc_macro2::TokenStream {
173 | let original_class_names: Vec = classes
174 | .keys()
175 | .map(|class| class.to_case(Case::Snake))
176 | .map(|class| quote::format_ident!("{}", class.as_str()))
177 | .collect();
178 |
179 | let randomized_class_names: Vec<&String> = classes.values().collect();
180 |
181 | let doc = original_class_names
182 | .iter()
183 | .zip(randomized_class_names.iter())
184 | .fold(String::new(), |mut doc, (variable, class_name)| {
185 | doc.push_str(&format!("{} = \"{}\"\n", variable, class_name));
186 | doc
187 | });
188 |
189 | quote::quote! {
190 | #[doc=#doc]
191 | pub struct ClassNames {
192 | #(pub #original_class_names: &'static str,)*
193 | }
194 | impl ClassNames {
195 | pub fn new() -> Self {
196 | Self {
197 | #(#original_class_names: #randomized_class_names,)*
198 | }
199 | }
200 | }
201 |
202 | (STYLE_SHEET, ClassNames::new())
203 | }
204 | }
205 |
206 | fn create_include_bytes(untracked_load_paths: Vec) -> proc_macro2::TokenStream {
207 | let untracked_load_path_values: Vec = untracked_load_paths
208 | .into_iter()
209 | .map(|item| format!("{}", item.as_path().display()))
210 | .collect();
211 |
212 | quote::quote! {
213 | #(const _: &[u8] = include_bytes!(#untracked_load_path_values);)*
214 | }
215 | }
216 |
217 | enum Error {
218 | Turf(turf_internals::Error),
219 | LoadPathTracking(turf_internals::LoadPathTrackingError),
220 | }
221 |
222 | struct ProcessedStyleSheet {
223 | untracked_load_paths: Vec,
224 | css: String,
225 | class_names: HashMap,
226 | }
227 |
228 | fn handle_style_sheet(style_sheet: StyleSheetKind) -> Result {
229 | let CompiledStyleSheet {
230 | css,
231 | class_names,
232 | original_style_sheet,
233 | } = turf_internals::style_sheet(style_sheet).map_err(Error::Turf)?;
234 |
235 | let untracked_load_paths = {
236 | let mut values =
237 | turf_internals::get_untracked_load_paths().map_err(Error::LoadPathTracking)?;
238 |
239 | if let StyleSheetKind::File(current_file_path) = original_style_sheet {
240 | values.push(current_file_path);
241 | }
242 |
243 | values
244 | };
245 |
246 | Ok(ProcessedStyleSheet {
247 | untracked_load_paths,
248 | css,
249 | class_names,
250 | })
251 | }
252 |
253 | #[cfg(test)]
254 | mod tests {
255 | use std::collections::HashMap;
256 |
257 | use super::create_classes_structure;
258 |
259 | #[test]
260 | fn test() {
261 | let mut class_names = HashMap::new();
262 | class_names.insert(String::from("test-class"), String::from("abc-123"));
263 |
264 | let out = create_classes_structure(class_names);
265 |
266 | assert_eq!(
267 | out.to_string(),
268 | quote::quote! {
269 | #[doc="TEST_CLASS = \"abc-123\"\n"]
270 | pub struct ClassName;
271 | impl ClassName {
272 | pub const TEST_CLASS: &'static str = "abc-123";
273 | }
274 | }
275 | .to_string()
276 | )
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # turf 🌱
2 |
3 | > **Warning** | The repository reflects the current development state, which may differ from the released version.
4 |
5 | `turf` allows you to build SCSS to CSS during compile time and inject those styles into your binary.
6 |
7 | [![Crates.io][crates-badge]][crates-url]
8 | [![Docs.rs][docs-badge]][docs-url]
9 | [![Build Status][actions-badge]][actions-url]
10 | [![MIT licensed][lic-badge]][lic-url]
11 |
12 | [crates-badge]: https://img.shields.io/crates/v/turf.svg
13 | [crates-url]: https://crates.io/crates/turf
14 | [docs-badge]: https://img.shields.io/docsrs/turf/latest.svg?logo=docsdotrs&label=docs.rs
15 | [docs-url]: https://docs.rs/turf
16 | [actions-badge]: https://github.com/myFavShrimp/turf/actions/workflows/rust-ci.yml/badge.svg
17 | [actions-url]: https://github.com/myFavShrimp/turf/actions/workflows/rust-ci.yml
18 | [lic-url]: https://github.com/myFavShrimp/turf/blob/master/LICENSE
19 | [lic-badge]: https://img.shields.io/badge/license-MIT-blue.svg
20 |
21 | **turf will:**
22 |
23 | - 🌿 transform your SCSS files into CSS with [grass](https://github.com/connorskees/grass/), right at compilation time
24 | - 🪴 generate unique and dynamic class names for your CSS during compilation
25 | - 🔬 minify and optimize your CSS using [lightningcss](https://github.com/parcel-bundler/lightningcss), ensuring compatibility with various browser targets
26 | - 🎨 inject the generated CSS into your binary, guaranteeing quick access to your styles whenever you need them
27 |
28 | ## Usage
29 |
30 | For a complete runnable example project, you can check out one of the examples:
31 |
32 | | [leptos-example](https://github.com/myFavShrimp/turf/tree/main/examples/leptos-example) | [yew-example](https://github.com/myFavShrimp/turf/tree/main/examples/yew-example) | [dioxus-example](https://github.com/myFavShrimp/turf/tree/main/examples/dioxus-example) | [axum-askama-htmx](https://github.com/myFavShrimp/turf/tree/main/examples/axum-askama-htmx) |
33 | | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
34 |
35 | **1. Create SCSS styles for your application**
36 |
37 | ```scss
38 | // file at scss/file/path.scss
39 |
40 | .TopLevelClass {
41 | color: red;
42 |
43 | .SomeClass {
44 | color: blue;
45 | }
46 | }
47 | ```
48 |
49 | **2. Use the `style_sheet` macro to include the resulting CSS in your code**
50 |
51 | ```rust,ignore
52 | turf::style_sheet!("scss/file/path.scss");
53 | ```
54 |
55 | The macro from the above example will expand to the following code:
56 |
57 | ```rust
58 | static STYLE_SHEET: &'static str = "";
59 | struct ClassName;
60 | impl ClassName {
61 | pub const TOP_LEVEL_CLASS: &'static str = "";
62 | pub const SOME_CLASS: &'static str = "";
63 | }
64 | ```
65 |
66 | **3. Use the `ClassName` struct and its associated constants to access the generated class names**
67 |
68 | ```rust,ignore
69 | let top_level_class_name = ClassName::TOP_LEVEL_CLASS;
70 | let some_class_name = ClassName::SOME_CLASS;
71 | ```
72 |
73 | ### Configuration
74 |
75 | The configuration for turf can be specified in the Cargo.toml file using the `[package.metadata.turf]` and `[package.metadata.turf-dev]` keys. This allows you to conveniently manage your SCSS compilation settings for both development and production builds within your project's manifest.
76 |
77 | Both profiles offer the exact same configuration options. However, if you haven't specified a `[package.metadata.turf-dev]` profile, the `[package.metadata.turf]` settings will also be applied to debug builds. This ensures consistency in the compilation process across different build types unless you explicitly define a separate configuration for the development profile.
78 |
79 | Example configuration:
80 |
81 | ```toml
82 | [package.metadata.turf]
83 | minify = true
84 | load_paths = ["path/to/shared/scss/files", "path/to/other/shared/scss/files"]
85 | browser_targets = [
86 | "defaults",
87 | "> 5%",
88 | "safari 12",
89 | ]
90 |
91 | [package.metadata.turf.class_names]
92 | template = "-with-custom-"
93 | excludes = ["exclude-this-class-please", "^abc-[123]{4}"]
94 |
95 | [package.metadata.turf.file_output]
96 | global_css_file_path = "path/to/global.css"
97 | separate_css_files_path = "dir/for/separate/css/"
98 | ```
99 |
100 | The following configuration options are available:
101 |
102 | - `minify` (default: `true`): Specifies whether the generated CSS should be minified or not. If set to true, the CSS output will be compressed and optimized for reduced file size. If set to false, the CSS output will be formatted with indentation and line breaks for improved readability.
103 |
104 | - `load_paths`: Specifies additional paths to search for SCSS files to include during compilation. It accepts a list of string values, where each value represents a directory path to be included. This option allows you to import SCSS files from multiple directories.
105 |
106 | - `browser_targets`: Defines the target browser versions for compatibility when generating CSS. It accepts an array of strings in [browserslist](https://browsersl.ist/) format (e.g., "defaults", "> 5%", "safari 12"). This ensures the generated CSS is compatible with the specified browser versions.
107 |
108 | - `class_names`: Allows configuration of the CSS class name generation. It expects a structure that contains two values for generating CSS class names and excluding class names from the uniquification process.
109 |
110 | - `debug` (default: `false`): When set to true, this option will enable debug output of the read configuration and the generated CSS class names. This can be helpful for troubleshooting and understanding how the CSS is being generated.
111 |
112 | - `file_output`: Enables output of compiled CSS. It expects a structure that contains two values for a single global CSS file or separate CSS files for each compiled SCSS file.
113 |
114 | #### The `class_names` Key
115 |
116 | - `template` (default: `"class-"`): Specifies the template for generating randomized CSS class names. The template can include placeholders to customize the output:
117 | - `` will be replaced with a unique identifier for each CSS class name
118 | - `` will be replaced with the original class name from the SCSS file
119 | - `` will be replaced with the hash of the original class name from the SCSS file
120 | - `` will be replaced with the first 5 characters of the hash of the original class name from the SCSS file
121 | - `` will be replaced with the hash of the SCSS file
122 | - `` will be replaced with the first 8 characters of the hash of the SCSS file
123 |
124 | - `excludes`: An array of regex patterns that exclude class names in your SCSS files from the class name uniquification process.
125 |
126 | #### The `file_output` Key
127 |
128 | - `global_css_file_path`: Specifies the file path for a global CSS file. If set, a CSS file will be created at the provided path, and all compiled styles will be written to this file. This allows you to have a single CSS file containing all the compiled styles.
129 |
130 | - `separate_css_files_path`: Specifies the directory path for separate CSS files. If set, all compiled CSS files will be saved in the specified directory. Each compiled SCSS file will have its corresponding CSS file in this directory, allowing for modular CSS management. The file name for inline SCSS style definitions will be a 64 bit hash that is computed from the original SCSS style.
131 |
132 | ### Additional Macros
133 |
134 | turf provides a few additional macros for other use cases.
135 |
136 | #### The `style_sheet_values` Macro
137 |
138 | In some cases, it may be necessary to have a struct's instance to access the class names (for example when using turf in [askama](https://github.com/djc/askama) templates).
139 | The `turf::style_sheet_values` macro provides an alternative to directly including the resulting CSS and obtaining the associated class names. It returns a tuple of `(style_sheet: &'static str, class_names: struct)`.
140 |
141 | **Usage:**
142 |
143 | ```rust,ignore
144 | let (style_sheet, class_names) = turf::style_sheet_values!("path/to/style.scss");
145 | let some_class_name = class_names.some_class;
146 | ```
147 |
148 | #### The `inline_style_sheet` Macro
149 |
150 | If you don't want your style sheet to live in another file, you can use the `turf::inline_style_sheet` macro. It allows you to write inline SCSS which will then be compiled to CSS.
151 |
152 | **Usage:**
153 |
154 | ```rust,ignore
155 | turf::inline_style_sheet! {
156 | .TopLevelClass {
157 | color: red;
158 |
159 | .SomeClass {
160 | color: blue;
161 | }
162 | }
163 | }
164 |
165 | // ...
166 |
167 | let some_class_name = ClassName::SOME_CLASS;
168 | ```
169 |
170 | #### The `inline_style_sheet_values` Macro
171 |
172 | This macro combines the functionality of both the `style_sheet_values` and `inline_style_sheet` macros. It allows you to write inline SCSS and returns an tuple of `(style_sheet: &'static str, class_names: struct)`.
173 |
174 | **Usage:**
175 |
176 | ```rust,ignore
177 | let (style_sheet, class_names) = turf::inline_style_sheet_values! {
178 | .TopLevelClass {
179 | color: red;
180 |
181 | .SomeClass {
182 | color: blue;
183 | }
184 | }
185 | };
186 | let some_class_name = class_names.some_class;
187 | ```
188 |
189 | ## Contributions
190 |
191 | Contributions to turf are always welcome! Whether you have ideas for new features or improvements, don't hesitate to open an issue or submit a pull request. 🤝
192 |
193 | ## License
194 |
195 | turf is licensed under the MIT license. For more details, please refer to the LICENSE file. 📄
196 |
--------------------------------------------------------------------------------
/turf_internals/src/settings.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use serde::Deserialize;
4 |
5 | use crate::{
6 | manifest::ManifestError,
7 | path_utils::{canonicalize, PathResolutionError},
8 | };
9 |
10 | #[derive(Deserialize, Debug, Default, Clone)]
11 | pub struct FileOutput {
12 | pub(crate) global_css_file_path: Option,
13 | pub(crate) separate_css_files_path: Option,
14 | }
15 |
16 | pub(crate) static DEFAULT_CLASS_NAME_TEMPLATE: &str =
17 | "class-";
18 |
19 | #[derive(Deserialize, Debug, Clone, PartialEq)]
20 | pub struct ClassNameGeneration {
21 | pub(crate) template: String,
22 | #[serde(default)]
23 | pub(crate) excludes: Vec,
24 | }
25 |
26 | impl Default for ClassNameGeneration {
27 | fn default() -> Self {
28 | Self {
29 | template: DEFAULT_CLASS_NAME_TEMPLATE.to_owned(),
30 | excludes: vec![],
31 | }
32 | }
33 | }
34 |
35 | pub(crate) static DEFAULT_MINIFY: bool = true;
36 |
37 | fn default_minify() -> bool {
38 | DEFAULT_MINIFY
39 | }
40 |
41 | #[derive(Deserialize, Debug, Clone)]
42 | pub struct Settings {
43 | #[serde(default)]
44 | pub(crate) debug: bool,
45 | #[serde(default = "default_minify")]
46 | pub(crate) minify: bool,
47 | #[serde(default)]
48 | pub(crate) load_paths: Vec,
49 | #[serde(default)]
50 | pub(crate) browser_targets: BrowserTargets,
51 | #[serde(default)]
52 | pub(crate) class_names: ClassNameGeneration,
53 | pub(crate) file_output: Option,
54 | }
55 |
56 | impl Default for Settings {
57 | fn default() -> Self {
58 | Self {
59 | debug: false,
60 | minify: DEFAULT_MINIFY,
61 | load_paths: Vec::new(),
62 | browser_targets: BrowserTargets(None),
63 | class_names: ClassNameGeneration::default(),
64 | file_output: None,
65 | }
66 | }
67 | }
68 |
69 | impl Settings {
70 | pub fn canonicalized_load_paths(&self) -> Result, PathResolutionError> {
71 | self.load_paths
72 | .clone()
73 | .into_iter()
74 | .map(canonicalize)
75 | .collect()
76 | }
77 | }
78 |
79 | impl<'a> TryFrom for grass::Options<'a> {
80 | type Error = PathResolutionError;
81 |
82 | fn try_from(val: Settings) -> Result {
83 | Ok(grass::Options::default()
84 | .style(grass::OutputStyle::Expanded)
85 | .load_paths(&val.canonicalized_load_paths()?))
86 | }
87 | }
88 |
89 | impl<'a> From for lightningcss::printer::PrinterOptions<'a> {
90 | fn from(val: Settings) -> Self {
91 | lightningcss::printer::PrinterOptions {
92 | minify: val.minify,
93 | project_root: None,
94 | targets: val.browser_targets.0.into(),
95 | analyze_dependencies: None,
96 | pseudo_classes: None,
97 | }
98 | }
99 | }
100 |
101 | #[derive(Deserialize, Debug, Default, Clone)]
102 | #[serde(try_from = "RawBrowserTargets")]
103 | pub struct BrowserTargets(pub Option);
104 |
105 | #[derive(Deserialize, Debug, Default, Clone)]
106 | pub struct RawBrowserTargets(Vec);
107 |
108 | #[derive(Debug, thiserror::Error)]
109 | #[error("Error reading browser_targets: {0:#?}")]
110 | pub struct FromRawTargetsErrorCollection(Vec);
111 |
112 | #[derive(thiserror::Error)]
113 | #[error("Failed to read browser target: {target:?} - {error}")]
114 | pub struct FromRawTargetsError {
115 | target: String,
116 | error: String,
117 | }
118 |
119 | impl std::fmt::Debug for FromRawTargetsError {
120 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 | f.write_str(&self.to_string())
122 | }
123 | }
124 |
125 | impl TryFrom for BrowserTargets {
126 | type Error = FromRawTargetsErrorCollection;
127 |
128 | fn try_from(value: RawBrowserTargets) -> Result {
129 | let errors = value
130 | .0
131 | .iter()
132 | .filter_map(|target| {
133 | match lightningcss::targets::Browsers::from_browserslist([target]) {
134 | Ok(_) => None,
135 | Err(e) => Some(FromRawTargetsError {
136 | error: e.to_string(),
137 | target: target.clone(),
138 | }),
139 | }
140 | })
141 | .collect::>();
142 |
143 | if !errors.is_empty() {
144 | return Err(FromRawTargetsErrorCollection(errors));
145 | }
146 |
147 | Ok(
148 | lightningcss::targets::Browsers::from_browserslist(value.0.clone())
149 | .map(BrowserTargets)
150 | .unwrap(),
151 | )
152 | }
153 | }
154 |
155 | static TURF_SETTINGS: std::sync::OnceLock = std::sync::OnceLock::new();
156 | static TURF_DEV_SETTINGS: std::sync::OnceLock = std::sync::OnceLock::new();
157 |
158 | #[derive(Debug, thiserror::Error)]
159 | #[error("Could not obtain turf settings from the Cargo manifest")]
160 | pub struct SettingsError(#[from] ManifestError);
161 |
162 | impl Settings {
163 | pub fn get() -> Result {
164 | let dev_settings = Self::dev_profile_settings()?;
165 | let prod_settings = Self::prod_profile_settings()?;
166 |
167 | Ok(Self::choose_settings(
168 | dev_settings,
169 | prod_settings,
170 | cfg!(debug_assertions),
171 | ))
172 | }
173 |
174 | fn choose_settings(
175 | dev: Option,
176 | prod: Option,
177 | is_debug_build: bool,
178 | ) -> Self {
179 | if let (Some(cfg), true) = (dev.or(prod.clone()), is_debug_build) {
180 | cfg
181 | } else if let (Some(cfg), false) = (prod, is_debug_build) {
182 | cfg
183 | } else {
184 | Settings::default()
185 | }
186 | }
187 |
188 | fn dev_profile_settings() -> Result