Skip to main content

react_auditor/
watch.rs

1use std::collections::HashSet;
2use std::path::Path;
3use std::sync::mpsc;
4use std::time::Duration;
5
6use anyhow::Result;
7use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
8
9use crate::formatters;
10use crate::scanner::Scanner;
11
12const DEBOUNCE_MS: u64 = 200;
13
14pub fn watch(scanner: &Scanner) -> Result<()> {
15    let dirs = resolve_watch_dirs(&scanner.files);
16    if dirs.is_empty() {
17        eprintln!("Nothing to watch. Specify a directory or file pattern.");
18        return Ok(());
19    }
20
21    let (tx, rx) = mpsc::channel::<Vec<String>>();
22
23    let mut watcher = RecommendedWatcher::new(
24        move |res: Result<Event, notify::Error>| {
25            if let Ok(event) = res {
26                let paths: Vec<String> = event
27                    .paths
28                    .iter()
29                    .filter_map(|p| {
30                        let ext = p.extension().and_then(|e| e.to_str())?;
31                        if matches!(ext, "js" | "jsx" | "ts" | "tsx") {
32                            Some(p.to_string_lossy().to_string())
33                        } else {
34                            None
35                        }
36                    })
37                    .collect();
38                if !paths.is_empty() {
39                    let _ = tx.send(paths);
40                }
41            }
42        },
43        Config::default(),
44    )?;
45
46    for dir in &dirs {
47        watcher.watch(dir, RecursiveMode::Recursive)?;
48    }
49
50    eprintln!(
51        "Watching {} director(ies) for changes. Ctrl+C to stop.",
52        dirs.len()
53    );
54
55    run_scan_loop(scanner, &rx)
56}
57
58fn run_scan_loop(scanner: &Scanner, rx: &mpsc::Receiver<Vec<String>>) -> Result<()> {
59    loop {
60        let mut changed = HashSet::new();
61
62        match rx.recv() {
63            Ok(paths) => {
64                for p in paths {
65                    changed.insert(p);
66                }
67            }
68            Err(_) => break,
69        }
70
71        loop {
72            match rx.recv_timeout(Duration::from_millis(DEBOUNCE_MS)) {
73                Ok(paths) => {
74                    for p in paths {
75                        changed.insert(p);
76                    }
77                }
78                Err(mpsc::RecvTimeoutError::Timeout) => break,
79                Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(()),
80            }
81        }
82
83        let changed: Vec<String> = changed.into_iter().collect();
84
85        if changed.is_empty() {
86            continue;
87        }
88
89        eprint!("\r[{} file(s) changed] Scanning...", changed.len());
90
91        match scanner.scan_paths(&changed) {
92            Ok(results) => {
93                let formatter = formatters::get_formatter("stylish");
94                let output = formatter.format(&results, false);
95                print!("{output}");
96            }
97            Err(e) => {
98                eprintln!("\rScan error: {e}");
99            }
100        }
101    }
102
103    Ok(())
104}
105
106fn resolve_watch_dirs(patterns: &[String]) -> Vec<Box<Path>> {
107    let mut dirs = Vec::new();
108    for pattern in patterns {
109        let path = Path::new(pattern);
110        if path.is_dir() {
111            dirs.push(path.into());
112        } else if path.is_file() {
113            if let Some(parent) = path.parent() {
114                dirs.push(parent.into());
115            }
116        } else {
117            if let Some(parent) = path.parent() {
118                if parent.to_string_lossy().is_empty() || parent == Path::new(".") {
119                    if let Ok(cwd) = std::env::current_dir() {
120                        dirs.push(cwd.into());
121                    }
122                } else if parent.is_dir() {
123                    dirs.push(parent.into());
124                }
125            }
126        }
127    }
128    dirs.sort();
129    dirs.dedup();
130    dirs
131}