ferridriver_script/
discover.rs1use std::path::{Path, PathBuf};
10
11pub const SOURCE_EXTENSIONS: &[&str] = &["js", "cjs", "mjs", "jsx", "ts", "cts", "mts", "tsx"];
15
16#[must_use]
18pub fn is_source_file(path: &Path) -> bool {
19 path
20 .extension()
21 .and_then(|e| e.to_str())
22 .is_some_and(|ext| SOURCE_EXTENSIONS.contains(&ext))
23}
24
25#[must_use]
30pub fn walk_source_files(dir: &Path) -> Vec<PathBuf> {
31 let mut out = Vec::new();
32 walk_into(dir, &mut out);
33 out.sort();
34 out.dedup();
35 out
36}
37
38fn walk_into(dir: &Path, out: &mut Vec<PathBuf>) {
39 let Ok(rd) = std::fs::read_dir(dir) else { return };
40 for entry in rd.flatten() {
41 let p = entry.path();
42 if p.is_dir() {
43 walk_into(&p, out);
44 } else if p.is_file() && is_source_file(&p) {
45 out.push(p);
46 }
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn accepts_the_full_source_set_rejects_others() {
56 for ext in ["js", "cjs", "mjs", "jsx", "ts", "cts", "mts", "tsx"] {
57 assert!(is_source_file(Path::new(&format!("a.{ext}"))), "{ext} should be source");
58 }
59 for ext in ["txt", "json", "map", ""] {
60 assert!(
61 !is_source_file(Path::new(&format!("a.{ext}"))),
62 "{ext} must not be source"
63 );
64 }
65 }
66
67 #[test]
68 fn walk_recurses_nested_directories() {
69 let tmp = tempfile::tempdir().expect("tempdir");
70 let root = tmp.path();
71 std::fs::create_dir_all(root.join("a/b")).unwrap();
72 std::fs::write(root.join("top.ts"), "").unwrap();
73 std::fs::write(root.join("a/mid.tsx"), "").unwrap();
74 std::fs::write(root.join("a/b/deep.cts"), "").unwrap();
75 std::fs::write(root.join("a/b/skip.txt"), "").unwrap();
76
77 let found = walk_source_files(root);
78 let names: Vec<_> = found
79 .iter()
80 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
81 .collect();
82 assert_eq!(
83 names,
84 vec!["deep.cts", "mid.tsx", "top.ts"],
85 "recursive + sorted, .txt excluded"
86 );
87 }
88}