node_prune/
lib.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4use anyhow::{anyhow, bail, Context, Result};
5use clap::Parser;
6use log::debug;
7use serde::Serialize;
8use tokio::fs;
9use walkdir::WalkDir;
10
11/// default prune files
12const DEFAULT_FILES: &'static str = r"
13Jenkinsfile,
14Makefile,
15Gulpfile.js,
16Gruntfile.js,
17gulpfile.js,
18.DS_Store,
19.tern-project,
20.gitattributes,
21.editorconfig,
22.eslintrc,
23eslint,
24.eslintrc.js,
25.eslintrc.json,
26.eslintrc.yml,
27.eslintignore,
28.stylelintrc,
29stylelint.config.js,
30.stylelintrc.json,
31.stylelintrc.yaml,
32.stylelintrc.yml,
33.stylelintrc.js,
34.htmllintrc,
35htmllint.js,
36.lint,
37.npmrc,
38.npmignore,
39.jshintrc,
40.flowconfig,
41.documentup.json,
42.yarn-metadata.json,
43.travis.yml,
44appveyor.yml,
45.gitlab-ci.yml,
46circle.yml,
47.coveralls.yml,
48CHANGES,
49changelog,
50License,
51LICENSE.txt,
52LICENSE,
53LICENSE-MIT,
54LICENSE.BSD,
55license,
56LICENCE.txt,
57LICENCE,
58LICENCE-MIT,
59LICENCE.BSD,
60licence,
61AUTHORS,
62CONTRIBUTORS,
63.yarn-integrity,
64.yarnclean,
65_config.yml,
66.babelrc,
67.yo-rc.json,
68jest.config.js,
69karma.conf.js,
70wallaby.js,
71wallaby.conf.js,
72.prettierrc,
73.prettierrc.yml,
74.prettierrc.toml,
75.prettierrc.js,
76.prettierrc.json,
77prettier.config.js,
78.appveyor.yml,
79tsconfig.json,
80tslint.json,
81";
82
83/// default prune directories
84const DEFAULT_DIRS: &'static str = r"
85__tests__,
86test,
87tests,
88testing,
89benchmark,
90powered-test,
91docs,
92doc,
93.idea,
94.vscode,
95website,
96images,
97assets,
98example,
99examples,
100coverage,
101.nyc_output,
102.circleci,
103.github,
104";
105
106/// default prune extensions
107const DEFAULT_EXTS: &'static str = r"
108markdown,
109md,
110mkd,
111ts,
112jst,
113coffee,
114tgz,
115swp,
116";
117
118#[derive(Parser)]
119pub struct Config {
120    #[clap(
121        short = 'p',
122        long = "path",
123        parse(from_os_str),
124        default_value = "node_modules"
125    )]
126    pub path: PathBuf,
127
128    #[clap(short = 'v', long = "verbose")]
129    pub verbose: bool,
130}
131
132#[derive(Debug, Serialize, Default)]
133pub struct Stats {
134    pub files_total: u64,
135    pub files_removed: u64,
136    pub removed_size: u64,
137}
138
139#[derive(Debug)]
140pub struct Prune {
141    pub dir: PathBuf,
142    files: HashSet<String>,
143    exts: HashSet<String>,
144    dirs: HashSet<String>,
145}
146
147impl Prune {
148    pub fn new() -> Self {
149        Self {
150            dir: PathBuf::from("node_modules"),
151            files: split(DEFAULT_FILES),
152            dirs: split(DEFAULT_DIRS),
153            exts: split(DEFAULT_EXTS),
154        }
155    }
156
157    pub async fn run(&self) -> Result<Stats> {
158        let mut stats: Stats = Default::default();
159
160        let mut walker = WalkDir::new(&self.dir).into_iter();
161        loop {
162            let entry = match walker.next() {
163                Some(Ok(entry)) => entry,
164                Some(Err(err)) => {
165                    bail!("access {} error", err.path().unwrap().display())
166                }
167                None => break,
168            };
169
170            let filepath = entry.path();
171            if !self.need_prune(filepath) {
172                debug!("skip: {}", filepath.display());
173                continue;
174            }
175
176            stats.files_total += 1;
177            stats.removed_size += entry.metadata().unwrap().len();
178
179            if filepath.is_dir() {
180                let s = dir_stats(filepath)?;
181                stats.files_total += s.files_total;
182                stats.files_removed += s.files_removed;
183                stats.removed_size += s.removed_size;
184
185                fs::remove_dir_all(filepath)
186                    .await
187                    .with_context(|| format!("removing directory {}", filepath.display()))?;
188
189                walker.skip_current_dir();
190                continue;
191            }
192
193            fs::remove_file(filepath)
194                .await
195                .with_context(|| format!("removing file {}", filepath.display()))?;
196        }
197
198        Ok(stats)
199    }
200
201    /// is filepath need prune
202    fn need_prune(&self, filepath: &Path) -> bool {
203        let filename = filepath.file_name().unwrap().to_str().unwrap();
204
205        if filepath.is_dir() {
206            return self.dirs.contains(filename);
207        }
208
209        if self.files.contains(filename) {
210            return true;
211        }
212
213        if let Some(extension) = filepath.extension() {
214            let ext = extension.to_str().unwrap();
215            if self.exts.contains(ext) {
216                return true;
217            }
218        }
219
220        false
221    }
222}
223
224/// statistics file count, file size in given directory.
225fn dir_stats(dir: &Path) -> Result<Stats, walkdir::Error> {
226    let walker = WalkDir::new(dir).into_iter().filter_map(|e| e.ok());
227
228    let mut stats: Stats = Default::default();
229
230    for entry in walker {
231        let metadata = entry.metadata()?;
232        stats.files_total += 1;
233        stats.files_removed += 1;
234        stats.removed_size += metadata.len();
235    }
236
237    Ok(stats)
238}
239
240/// split string by comma
241///
242/// it will return `HashSet<String>`,items in HashSet has trimed
243fn split(paths: &str) -> HashSet<String> {
244    paths
245        .split(",")
246        .map(|x| x.trim().to_string())
247        .filter(|x| !x.is_empty())
248        .collect()
249}
250
251#[cfg(test)]
252mod tests {
253
254    use super::{dir_stats, split};
255    use std::path::Path;
256
257    #[test]
258    fn dir_stats_happy_path() {
259        let path = Path::new("src");
260        let stats = dir_stats(path).unwrap();
261        assert_eq!(stats.files_total, 4);
262        assert_eq!(stats.files_removed, 4);
263    }
264
265    #[test]
266    fn dir_not_exits() {
267        let path = Path::new("not_exist");
268        let stats = dir_stats(path).unwrap();
269        assert_eq!(stats.files_removed, 0);
270        assert_eq!(stats.files_total, 0);
271        assert_eq!(stats.files_removed, 0);
272    }
273
274    #[test]
275    fn split_happypath() {
276        let paths = String::from("prettier,eslint,typescript,prettier");
277        let files = split(&paths);
278        assert_eq!(files.len(), 3);
279    }
280
281    #[test]
282    fn split_string_with_consecutive_commas() {
283        let paths = String::from("prettier,,");
284        let files = split(&paths[..]);
285        assert_eq!(files.len(), 1);
286    }
287
288    #[test]
289    fn split_string_with_trim() {
290        let paths = String::from(" prettier ,javascript es6");
291        let files = split(&paths[..]);
292        assert_eq!(files.len(), 2);
293        assert!(files.contains("prettier"));
294        assert!(files.contains("javascript es6"));
295    }
296}