1use std::path::Path;
2
3use crate::types::ProjectType;
4
5const MARKERS: &[(&str, ProjectType)] = &[
7 ("Cargo.toml", ProjectType::Rust),
8 ("pubspec.yaml", ProjectType::Flutter),
9 ("build.gradle", ProjectType::Gradle),
10 ("build.gradle.kts", ProjectType::Gradle),
11 ("pom.xml", ProjectType::Maven),
12 ("Package.swift", ProjectType::Swift),
13 ("CMakeLists.txt", ProjectType::CMake),
14 ("build.zig", ProjectType::Zig),
15 ("turbo.json", ProjectType::Turborepo),
16];
17
18const PACKAGE_JSON_MARKERS: &[(&str, ProjectType)] = &[
20 ("next.config.js", ProjectType::NextJs),
21 ("next.config.mjs", ProjectType::NextJs),
22 ("next.config.ts", ProjectType::NextJs),
23];
24
25pub fn detect_project_type(dir: &Path) -> Option<ProjectType> {
27 let entries: Vec<String> = std::fs::read_dir(dir)
28 .ok()?
29 .filter_map(|e| e.ok())
30 .filter_map(|e| e.file_name().into_string().ok())
31 .collect();
32
33 for (marker, project_type) in MARKERS {
35 if entries.iter().any(|e| e == *marker) {
36 return Some(project_type.clone());
37 }
38 }
39
40 if entries
42 .iter()
43 .any(|e| e.ends_with(".csproj") || e.ends_with(".fsproj"))
44 {
45 return Some(ProjectType::DotNet);
46 }
47
48 if entries.iter().any(|e| {
50 e == "pyproject.toml" || e == "setup.py" || e == "requirements.txt" || e == "Pipfile"
51 }) {
52 return Some(ProjectType::Python);
53 }
54
55 if entries.iter().any(|e| e == "go.mod") {
57 return Some(ProjectType::Go);
58 }
59
60 if entries.iter().any(|e| e == "package.json") {
62 for (marker, project_type) in PACKAGE_JSON_MARKERS {
64 if entries.iter().any(|e| e == *marker) {
65 return Some(project_type.clone());
66 }
67 }
68
69 if entries.iter().any(|e| e == "ios" || e == "android") {
71 let has_ios_dir = dir.join("ios").is_dir();
72 let has_android_dir = dir.join("android").is_dir();
73 if has_ios_dir || has_android_dir {
74 return Some(ProjectType::ReactNative);
75 }
76 }
77
78 if entries.iter().any(|e| e == "Podfile") {
80 return Some(ProjectType::CocoaPods);
81 }
82
83 return Some(ProjectType::Node);
84 }
85
86 None
87}
88
89pub fn artifact_dirs(project_type: &ProjectType) -> &'static [&'static str] {
91 match project_type {
92 ProjectType::Rust => &["target"],
93 ProjectType::Node => &["node_modules", ".angular"],
94 ProjectType::Flutter => &[".dart_tool", "build"],
95 ProjectType::Gradle => &["build", ".gradle"],
96 ProjectType::Maven => &["target"],
97 ProjectType::Swift => &[".build"],
98 ProjectType::CMake => &["build"],
99 ProjectType::DotNet => &["bin", "obj"],
100 ProjectType::Python => &[
101 "__pycache__",
102 ".venv",
103 "venv",
104 ".mypy_cache",
105 ".pytest_cache",
106 ".ruff_cache",
107 ".tox",
108 ],
109 ProjectType::Go => &[],
110 ProjectType::Zig => &["zig-cache", "zig-out"],
111 ProjectType::Turborepo => &[".turbo"],
112 ProjectType::NextJs => &["node_modules", ".next"],
113 ProjectType::CocoaPods => &["Pods"],
114 ProjectType::ReactNative => &["node_modules", "ios/Pods", "android/build", ".expo"],
115 }
116}
117
118pub fn rebuild_hint(project_type: &ProjectType) -> &'static str {
120 match project_type {
121 ProjectType::Rust => "cargo build",
122 ProjectType::Node => "npm install",
123 ProjectType::Flutter => "flutter pub get",
124 ProjectType::Gradle => "gradle build",
125 ProjectType::Maven => "mvn install",
126 ProjectType::Swift => "swift build",
127 ProjectType::CMake => "cmake --build .",
128 ProjectType::DotNet => "dotnet restore",
129 ProjectType::Python => "pip install -r requirements.txt",
130 ProjectType::Go => "go mod download",
131 ProjectType::Zig => "zig build",
132 ProjectType::Turborepo => "turbo run build",
133 ProjectType::NextJs => "npm install && npm run build",
134 ProjectType::CocoaPods => "pod install",
135 ProjectType::ReactNative => "npm install && cd ios && pod install",
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use std::fs;
143
144 fn temp_project(name: &str, markers: &[&str]) -> std::path::PathBuf {
145 let dir = std::env::temp_dir().join(format!("diskforge_test_{name}"));
146 let _ = fs::remove_dir_all(&dir);
147 fs::create_dir_all(&dir).unwrap();
148 for marker in markers {
149 fs::write(dir.join(marker), "").unwrap();
150 }
151 dir
152 }
153
154 fn cleanup(dir: &std::path::Path) {
155 let _ = fs::remove_dir_all(dir);
156 }
157
158 #[test]
159 fn detect_rust() {
160 let dir = temp_project("rust", &["Cargo.toml"]);
161 assert_eq!(detect_project_type(&dir), Some(ProjectType::Rust));
162 cleanup(&dir);
163 }
164
165 #[test]
166 fn detect_flutter() {
167 let dir = temp_project("flutter", &["pubspec.yaml"]);
168 assert_eq!(detect_project_type(&dir), Some(ProjectType::Flutter));
169 cleanup(&dir);
170 }
171
172 #[test]
173 fn detect_gradle() {
174 let dir = temp_project("gradle", &["build.gradle"]);
175 assert_eq!(detect_project_type(&dir), Some(ProjectType::Gradle));
176 cleanup(&dir);
177 }
178
179 #[test]
180 fn detect_node() {
181 let dir = temp_project("node", &["package.json"]);
182 assert_eq!(detect_project_type(&dir), Some(ProjectType::Node));
183 cleanup(&dir);
184 }
185
186 #[test]
187 fn detect_nextjs() {
188 let dir = temp_project("nextjs", &["package.json", "next.config.js"]);
189 assert_eq!(detect_project_type(&dir), Some(ProjectType::NextJs));
190 cleanup(&dir);
191 }
192
193 #[test]
194 fn detect_python() {
195 let dir = temp_project("python", &["pyproject.toml"]);
196 assert_eq!(detect_project_type(&dir), Some(ProjectType::Python));
197 cleanup(&dir);
198 }
199
200 #[test]
201 fn detect_go() {
202 let dir = temp_project("go", &["go.mod"]);
203 assert_eq!(detect_project_type(&dir), Some(ProjectType::Go));
204 cleanup(&dir);
205 }
206
207 #[test]
208 fn detect_dotnet() {
209 let dir = temp_project("dotnet", &["MyApp.csproj"]);
210 assert_eq!(detect_project_type(&dir), Some(ProjectType::DotNet));
211 cleanup(&dir);
212 }
213
214 #[test]
215 fn detect_empty_dir() {
216 let dir = temp_project("empty", &[]);
217 assert_eq!(detect_project_type(&dir), None);
218 cleanup(&dir);
219 }
220
221 #[test]
222 fn artifact_dirs_and_hints_for_all_types() {
223 let all = [
224 ProjectType::Rust,
225 ProjectType::Node,
226 ProjectType::Flutter,
227 ProjectType::Gradle,
228 ProjectType::Maven,
229 ProjectType::Swift,
230 ProjectType::CMake,
231 ProjectType::DotNet,
232 ProjectType::Python,
233 ProjectType::Go,
234 ProjectType::Zig,
235 ProjectType::Turborepo,
236 ProjectType::NextJs,
237 ProjectType::CocoaPods,
238 ProjectType::ReactNative,
239 ];
240 for pt in &all {
241 let dirs = artifact_dirs(pt);
242 let hint = rebuild_hint(pt);
243 assert!(!hint.is_empty(), "{pt:?} has empty rebuild hint");
244 let _ = dirs;
245 }
246 }
247}