use std::path::{Path, PathBuf};
pub const SOURCE_EXTENSIONS: &[&str] = &["js", "cjs", "mjs", "jsx", "ts", "cts", "mts", "tsx"];
#[must_use]
pub fn is_source_file(path: &Path) -> bool {
path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| SOURCE_EXTENSIONS.contains(&ext))
}
#[must_use]
pub fn walk_source_files(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
walk_into(dir, &mut out);
out.sort();
out.dedup();
out
}
fn walk_into(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for entry in rd.flatten() {
let p = entry.path();
if p.is_dir() {
walk_into(&p, out);
} else if p.is_file() && is_source_file(&p) {
out.push(p);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_the_full_source_set_rejects_others() {
for ext in ["js", "cjs", "mjs", "jsx", "ts", "cts", "mts", "tsx"] {
assert!(is_source_file(Path::new(&format!("a.{ext}"))), "{ext} should be source");
}
for ext in ["txt", "json", "map", ""] {
assert!(
!is_source_file(Path::new(&format!("a.{ext}"))),
"{ext} must not be source"
);
}
}
#[test]
fn walk_recurses_nested_directories() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::create_dir_all(root.join("a/b")).unwrap();
std::fs::write(root.join("top.ts"), "").unwrap();
std::fs::write(root.join("a/mid.tsx"), "").unwrap();
std::fs::write(root.join("a/b/deep.cts"), "").unwrap();
std::fs::write(root.join("a/b/skip.txt"), "").unwrap();
let found = walk_source_files(root);
let names: Vec<_> = found
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(
names,
vec!["deep.cts", "mid.tsx", "top.ts"],
"recursive + sorted, .txt excluded"
);
}
}