stylance_cli/
lib.rs

1use std::{
2    borrow::Cow,
3    collections::HashMap,
4    fs::{self, File},
5    io::{BufWriter, Write},
6    path::Path,
7};
8
9use anyhow::bail;
10pub use stylance_core::Config;
11use stylance_core::ModifyCssResult;
12use walkdir::WalkDir;
13
14pub fn run(manifest_dir: &Path, config: &Config) -> anyhow::Result<()> {
15    println!("Running stylance");
16    run_silent(manifest_dir, config, |file_path| {
17        println!("{}", file_path.display())
18    })
19}
20
21pub fn run_silent(
22    manifest_dir: &Path,
23    config: &Config,
24    mut file_visit_callback: impl FnMut(&Path),
25) -> anyhow::Result<()> {
26    let mut modified_css_files = Vec::new();
27
28    for folder in &config.folders {
29        for (entry, meta) in WalkDir::new(manifest_dir.join(folder))
30            .into_iter()
31            .filter_map(|e| e.ok())
32            .filter_map(|entry| entry.metadata().ok().map(|meta| (entry, meta)))
33        {
34            if meta.is_file() {
35                let path_str = entry.path().to_string_lossy();
36                if config.extensions.iter().any(|ext| path_str.ends_with(ext)) {
37                    file_visit_callback(entry.path());
38                    modified_css_files.push(stylance_core::load_and_modify_css(
39                        manifest_dir,
40                        entry.path(),
41                        config,
42                    )?);
43                }
44            }
45        }
46    }
47
48    {
49        // Verify that there are no hash collisions
50        let mut map = HashMap::new();
51        for file in modified_css_files.iter() {
52            if let Some(previous_file) = map.insert(&file.hash, file) {
53                bail!(
54                    "The following files had a hash collision:\n{}\n{}\nConsider increasing the hash_len setting.",
55                    file.path.to_string_lossy(),
56                    previous_file.path.to_string_lossy()
57                );
58            }
59        }
60    }
61
62    {
63        // sort by (filename, path)
64        fn key(a: &ModifyCssResult) -> (&std::ffi::OsStr, &String) {
65            (
66                a.path.file_name().expect("should be a file"),
67                &a.normalized_path_str,
68            )
69        }
70        modified_css_files.sort_unstable_by(|a, b| key(a).cmp(&key(b)));
71    }
72
73    if let Some(output_file) = &config.output_file {
74        if let Some(parent) = output_file.parent() {
75            fs::create_dir_all(parent)?;
76        }
77
78        let mut file = BufWriter::new(File::create(output_file)?);
79
80        if let Some(scss_prelude) = &config.scss_prelude {
81            if output_file
82                .extension()
83                .filter(|ext| ext.to_string_lossy() == "scss")
84                .is_some()
85            {
86                file.write_all(scss_prelude.as_bytes())?;
87                file.write_all(b"\n\n")?;
88            }
89        }
90
91        file.write_all(
92            modified_css_files
93                .iter()
94                .map(|r| r.contents.as_ref())
95                .collect::<Vec<_>>()
96                .join("\n\n")
97                .as_bytes(),
98        )?;
99    }
100
101    if let Some(output_dir) = &config.output_dir {
102        let output_dir = output_dir.join("stylance");
103        fs::create_dir_all(&output_dir)?;
104
105        let entries = fs::read_dir(&output_dir)?;
106
107        for entry in entries {
108            let entry = entry?;
109            let file_type = entry.file_type()?;
110
111            if file_type.is_file() {
112                fs::remove_file(entry.path())?;
113            }
114        }
115
116        let mut new_files = Vec::new();
117        for modified_css in modified_css_files {
118            let extension = modified_css
119                .path
120                .extension()
121                .map(|e| e.to_string_lossy())
122                .filter(|e| e == "css")
123                .unwrap_or(Cow::from("scss"));
124
125            let new_file_name = format!(
126                "{}-{}.{extension}",
127                modified_css
128                    .path
129                    .file_stem()
130                    .expect("This path should be a file")
131                    .to_string_lossy(),
132                modified_css.hash
133            );
134
135            new_files.push(new_file_name.clone());
136
137            let file_path = output_dir.join(new_file_name);
138            let mut file = BufWriter::new(File::create(file_path)?);
139
140            if let Some(scss_prelude) = &config.scss_prelude {
141                if extension == "scss" {
142                    file.write_all(scss_prelude.as_bytes())?;
143                    file.write_all(b"\n\n")?;
144                }
145            }
146
147            file.write_all(modified_css.contents.as_bytes())?;
148        }
149
150        let mut file = File::create(output_dir.join("_index.scss"))?;
151        file.write_all(
152            new_files
153                .iter()
154                .map(|f| format!("@use \"{f}\";\n"))
155                .collect::<Vec<_>>()
156                .join("")
157                .as_bytes(),
158        )?;
159    }
160
161    Ok(())
162}