use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ScanError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid project structure: {0}")]
InvalidProject(String),
}
#[derive(Debug, Clone)]
pub struct ProjectInfo {
pub root_path: PathBuf,
pub src_tauri_path: PathBuf,
pub tauri_config_path: Option<PathBuf>,
}
pub struct ProjectScanner {
current_dir: PathBuf,
}
impl ProjectScanner {
pub fn new() -> Self {
Self {
current_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
}
}
pub fn with_current_dir<P: AsRef<Path>>(path: P) -> Self {
Self {
current_dir: path.as_ref().to_path_buf(),
}
}
pub fn detect_project(&self) -> Result<Option<ProjectInfo>, ScanError> {
let mut current = self.current_dir.clone();
loop {
if let Some(project_info) = self.check_directory(¤t)? {
return Ok(Some(project_info));
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
Ok(None)
}
fn check_directory(&self, dir: &Path) -> Result<Option<ProjectInfo>, ScanError> {
let tauri_config_json = dir.join("tauri.conf.json");
let tauri_config_js = dir.join("tauri.conf.js");
let src_tauri = dir.join("src-tauri");
let tauri_config_path = if tauri_config_json.exists() {
Some(tauri_config_json)
} else if tauri_config_js.exists() {
Some(tauri_config_js)
} else {
None
};
if tauri_config_path.is_some() || src_tauri.exists() {
let src_tauri_path = if src_tauri.exists() && src_tauri.is_dir() {
src_tauri
} else if let Some(ref config_path) = tauri_config_path {
if let Ok(source_dir) = self.read_source_dir_from_config(config_path) {
dir.join(source_dir)
} else {
src_tauri
}
} else {
src_tauri
};
return Ok(Some(ProjectInfo {
root_path: dir.to_path_buf(),
src_tauri_path,
tauri_config_path,
}));
}
Ok(None)
}
fn read_source_dir_from_config(&self, config_path: &Path) -> Result<String, ScanError> {
let content = fs::read_to_string(config_path)?;
if config_path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(config) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(build) = config.get("build") {
if let Some(dev_path) = build.get("devPath").and_then(|v| v.as_str()) {
return Ok(dev_path.to_string());
}
}
}
}
Ok("src-tauri".to_string())
}
pub fn discover_rust_files(
&self,
project_info: &ProjectInfo,
) -> Result<Vec<PathBuf>, ScanError> {
let mut rust_files = Vec::new();
Self::walk_directory(&project_info.src_tauri_path, &mut rust_files)?;
Ok(rust_files)
}
fn walk_directory(dir: &Path, rust_files: &mut Vec<PathBuf>) -> Result<(), ScanError> {
if !dir.exists() || !dir.is_dir() {
return Ok(());
}
let entries = fs::read_dir(dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !["target", "node_modules", ".git", "dist"].contains(&dir_name) {
Self::walk_directory(&path, rust_files)?;
}
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
rust_files.push(path);
}
}
Ok(())
}
pub fn has_frontend(&self, project_info: &ProjectInfo) -> bool {
let package_json = project_info.root_path.join("package.json");
package_json.exists()
}
pub fn get_recommended_output_path(&self, project_info: &ProjectInfo) -> String {
if self.has_frontend(project_info) {
"./src/generated".to_string()
} else {
"./generated".to_string()
}
}
}
impl Default for ProjectScanner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_detect_tauri_project_with_config() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("tauri.conf.json");
fs::write(&config_path, r#"{"build": {"devPath": "./src"}}"#).unwrap();
let scanner = ProjectScanner::with_current_dir(temp_dir.path());
let project_info = scanner.detect_project().unwrap().unwrap();
assert_eq!(project_info.root_path, temp_dir.path());
assert!(project_info.tauri_config_path.is_some());
}
#[test]
fn test_detect_tauri_project_with_src_tauri() {
let temp_dir = TempDir::new().unwrap();
let src_tauri = temp_dir.path().join("src-tauri");
fs::create_dir(&src_tauri).unwrap();
let scanner = ProjectScanner::with_current_dir(temp_dir.path());
let project_info = scanner.detect_project().unwrap().unwrap();
assert_eq!(project_info.root_path, temp_dir.path());
assert_eq!(project_info.src_tauri_path, src_tauri);
}
#[test]
fn test_no_tauri_project() {
let temp_dir = TempDir::new().unwrap();
let scanner = ProjectScanner::with_current_dir(temp_dir.path());
let project_info = scanner.detect_project().unwrap();
assert!(project_info.is_none());
}
#[test]
fn test_discover_rust_files() {
let temp_dir = TempDir::new().unwrap();
let src_tauri = temp_dir.path().join("src-tauri");
fs::create_dir(&src_tauri).unwrap();
let main_rs = src_tauri.join("main.rs");
let lib_rs = src_tauri.join("lib.rs");
fs::write(&main_rs, "// main").unwrap();
fs::write(&lib_rs, "// lib").unwrap();
let project_info = ProjectInfo {
root_path: temp_dir.path().to_path_buf(),
src_tauri_path: src_tauri,
tauri_config_path: None,
};
let scanner = ProjectScanner::new();
let rust_files = scanner.discover_rust_files(&project_info).unwrap();
assert_eq!(rust_files.len(), 2);
assert!(rust_files.contains(&main_rs));
assert!(rust_files.contains(&lib_rs));
}
#[test]
fn test_has_frontend_detection() {
let temp_dir = TempDir::new().unwrap();
let package_json = temp_dir.path().join("package.json");
fs::write(&package_json, r#"{"name": "test"}"#).unwrap();
let project_info = ProjectInfo {
root_path: temp_dir.path().to_path_buf(),
src_tauri_path: temp_dir.path().join("src-tauri"),
tauri_config_path: None,
};
let scanner = ProjectScanner::new();
assert!(scanner.has_frontend(&project_info));
}
#[test]
fn test_recommended_output_path() {
let temp_dir = TempDir::new().unwrap();
let project_info = ProjectInfo {
root_path: temp_dir.path().to_path_buf(),
src_tauri_path: temp_dir.path().join("src-tauri"),
tauri_config_path: None,
};
let scanner = ProjectScanner::new();
assert_eq!(
scanner.get_recommended_output_path(&project_info),
"./generated"
);
let package_json = temp_dir.path().join("package.json");
fs::write(&package_json, r#"{"name": "test"}"#).unwrap();
assert_eq!(
scanner.get_recommended_output_path(&project_info),
"./src/generated"
);
}
}