tauri_typegen/build/
project_scanner.rs1use std::fs;
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum ScanError {
7 #[error("IO error: {0}")]
8 Io(#[from] std::io::Error),
9 #[error("Invalid project structure: {0}")]
10 InvalidProject(String),
11}
12
13#[derive(Debug, Clone)]
14pub struct ProjectInfo {
15 pub root_path: PathBuf,
16 pub src_tauri_path: PathBuf,
17 pub tauri_config_path: Option<PathBuf>,
18}
19
20pub struct ProjectScanner {
21 current_dir: PathBuf,
22}
23
24impl ProjectScanner {
25 pub fn new() -> Self {
26 Self {
27 current_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
28 }
29 }
30
31 pub fn with_current_dir<P: AsRef<Path>>(path: P) -> Self {
32 Self {
33 current_dir: path.as_ref().to_path_buf(),
34 }
35 }
36
37 pub fn detect_project(&self) -> Result<Option<ProjectInfo>, ScanError> {
39 let mut current = self.current_dir.clone();
41
42 loop {
43 if let Some(project_info) = self.check_directory(¤t)? {
44 return Ok(Some(project_info));
45 }
46
47 if let Some(parent) = current.parent() {
49 current = parent.to_path_buf();
50 } else {
51 break;
53 }
54 }
55
56 Ok(None)
57 }
58
59 fn check_directory(&self, dir: &Path) -> Result<Option<ProjectInfo>, ScanError> {
61 let tauri_config_json = dir.join("tauri.conf.json");
63 let tauri_config_js = dir.join("tauri.conf.js");
64 let src_tauri = dir.join("src-tauri");
65
66 let tauri_config_path = if tauri_config_json.exists() {
67 Some(tauri_config_json)
68 } else if tauri_config_js.exists() {
69 Some(tauri_config_js)
70 } else {
71 None
72 };
73
74 if tauri_config_path.is_some() || src_tauri.exists() {
76 let src_tauri_path = if src_tauri.exists() && src_tauri.is_dir() {
78 src_tauri
79 } else if let Some(ref config_path) = tauri_config_path {
80 if let Ok(source_dir) = self.read_source_dir_from_config(config_path) {
82 dir.join(source_dir)
83 } else {
84 src_tauri
86 }
87 } else {
88 src_tauri
89 };
90
91 return Ok(Some(ProjectInfo {
92 root_path: dir.to_path_buf(),
93 src_tauri_path,
94 tauri_config_path,
95 }));
96 }
97
98 Ok(None)
99 }
100
101 fn read_source_dir_from_config(&self, config_path: &Path) -> Result<String, ScanError> {
103 let content = fs::read_to_string(config_path)?;
104
105 if config_path.extension().and_then(|s| s.to_str()) == Some("json") {
107 if let Ok(config) = serde_json::from_str::<serde_json::Value>(&content) {
108 if let Some(build) = config.get("build") {
109 if let Some(dev_path) = build.get("devPath").and_then(|v| v.as_str()) {
110 return Ok(dev_path.to_string());
111 }
112 }
113 }
114 }
115
116 Ok("src-tauri".to_string())
118 }
119
120 pub fn discover_rust_files(
122 &self,
123 project_info: &ProjectInfo,
124 ) -> Result<Vec<PathBuf>, ScanError> {
125 let mut rust_files = Vec::new();
126 Self::walk_directory(&project_info.src_tauri_path, &mut rust_files)?;
127 Ok(rust_files)
128 }
129
130 fn walk_directory(dir: &Path, rust_files: &mut Vec<PathBuf>) -> Result<(), ScanError> {
132 if !dir.exists() || !dir.is_dir() {
133 return Ok(());
134 }
135
136 let entries = fs::read_dir(dir)?;
137
138 for entry in entries {
139 let entry = entry?;
140 let path = entry.path();
141
142 if path.is_dir() {
143 let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
145
146 if !["target", "node_modules", ".git", "dist"].contains(&dir_name) {
147 Self::walk_directory(&path, rust_files)?;
148 }
149 } else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
150 rust_files.push(path);
151 }
152 }
153
154 Ok(())
155 }
156
157 pub fn has_frontend(&self, project_info: &ProjectInfo) -> bool {
159 let package_json = project_info.root_path.join("package.json");
160 package_json.exists()
161 }
162
163 pub fn get_recommended_output_path(&self, project_info: &ProjectInfo) -> String {
165 if self.has_frontend(project_info) {
166 "./src/generated".to_string()
168 } else {
169 "./generated".to_string()
171 }
172 }
173}
174
175impl Default for ProjectScanner {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use std::fs;
185 use tempfile::TempDir;
186
187 #[test]
188 fn test_detect_tauri_project_with_config() {
189 let temp_dir = TempDir::new().unwrap();
190 let config_path = temp_dir.path().join("tauri.conf.json");
191 fs::write(&config_path, r#"{"build": {"devPath": "./src"}}"#).unwrap();
192
193 let scanner = ProjectScanner::with_current_dir(temp_dir.path());
194 let project_info = scanner.detect_project().unwrap().unwrap();
195
196 assert_eq!(project_info.root_path, temp_dir.path());
197 assert!(project_info.tauri_config_path.is_some());
198 }
199
200 #[test]
201 fn test_detect_tauri_project_with_src_tauri() {
202 let temp_dir = TempDir::new().unwrap();
203 let src_tauri = temp_dir.path().join("src-tauri");
204 fs::create_dir(&src_tauri).unwrap();
205
206 let scanner = ProjectScanner::with_current_dir(temp_dir.path());
207 let project_info = scanner.detect_project().unwrap().unwrap();
208
209 assert_eq!(project_info.root_path, temp_dir.path());
210 assert_eq!(project_info.src_tauri_path, src_tauri);
211 }
212
213 #[test]
214 fn test_no_tauri_project() {
215 let temp_dir = TempDir::new().unwrap();
216
217 let scanner = ProjectScanner::with_current_dir(temp_dir.path());
218 let project_info = scanner.detect_project().unwrap();
219
220 assert!(project_info.is_none());
221 }
222
223 #[test]
224 fn test_discover_rust_files() {
225 let temp_dir = TempDir::new().unwrap();
226 let src_tauri = temp_dir.path().join("src-tauri");
227 fs::create_dir(&src_tauri).unwrap();
228
229 let main_rs = src_tauri.join("main.rs");
230 let lib_rs = src_tauri.join("lib.rs");
231 fs::write(&main_rs, "// main").unwrap();
232 fs::write(&lib_rs, "// lib").unwrap();
233
234 let project_info = ProjectInfo {
235 root_path: temp_dir.path().to_path_buf(),
236 src_tauri_path: src_tauri,
237 tauri_config_path: None,
238 };
239
240 let scanner = ProjectScanner::new();
241 let rust_files = scanner.discover_rust_files(&project_info).unwrap();
242
243 assert_eq!(rust_files.len(), 2);
244 assert!(rust_files.contains(&main_rs));
245 assert!(rust_files.contains(&lib_rs));
246 }
247
248 #[test]
249 fn test_has_frontend_detection() {
250 let temp_dir = TempDir::new().unwrap();
251 let package_json = temp_dir.path().join("package.json");
252 fs::write(&package_json, r#"{"name": "test"}"#).unwrap();
253
254 let project_info = ProjectInfo {
255 root_path: temp_dir.path().to_path_buf(),
256 src_tauri_path: temp_dir.path().join("src-tauri"),
257 tauri_config_path: None,
258 };
259
260 let scanner = ProjectScanner::new();
261 assert!(scanner.has_frontend(&project_info));
262 }
263
264 #[test]
265 fn test_recommended_output_path() {
266 let temp_dir = TempDir::new().unwrap();
267
268 let project_info = ProjectInfo {
270 root_path: temp_dir.path().to_path_buf(),
271 src_tauri_path: temp_dir.path().join("src-tauri"),
272 tauri_config_path: None,
273 };
274
275 let scanner = ProjectScanner::new();
276 assert_eq!(
277 scanner.get_recommended_output_path(&project_info),
278 "./generated"
279 );
280
281 let package_json = temp_dir.path().join("package.json");
283 fs::write(&package_json, r#"{"name": "test"}"#).unwrap();
284
285 assert_eq!(
286 scanner.get_recommended_output_path(&project_info),
287 "./src/generated"
288 );
289 }
290}