use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct LoctreeConfig {
pub tauri: TauriConfig,
#[serde(default)]
pub library_mode: bool,
#[serde(default)]
pub library_example_globs: Vec<String>,
#[serde(default)]
pub analyzer: AnalyzerConfig,
#[serde(default)]
pub event_wrappers: Vec<String>,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct AnalyzerConfig {
#[serde(default = "default_parser_strategy")]
pub parser: String,
}
fn default_parser_strategy() -> String {
"oxc".to_string()
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct TauriConfig {
#[serde(default)]
pub command_macros: Vec<String>,
#[serde(default)]
pub dom_exclusions: Vec<String>,
#[serde(default)]
pub non_invoke_exclusions: Vec<String>,
#[serde(default)]
pub invalid_command_names: Vec<String>,
}
impl LoctreeConfig {
pub fn load(root: &Path) -> Self {
let config_path = crate::snapshot::project_config_dir(root).join("config.toml");
Self::load_from_path(&config_path)
}
pub fn load_from_path(path: &Path) -> Self {
if !path.exists() {
return Self::default();
}
match std::fs::read_to_string(path) {
Ok(content) => match toml::from_str(&content) {
Ok(config) => config,
Err(e) => {
eprintln!("[loctree][warn] Failed to parse {}: {}", path.display(), e);
Self::default()
}
},
Err(e) => {
eprintln!("[loctree][warn] Failed to read {}: {}", path.display(), e);
Self::default()
}
}
}
pub fn has_custom_command_macros(&self) -> bool {
!self.tauri.command_macros.is_empty()
}
pub fn parser_strategy(&self) -> &'static str {
if let Ok(env) = std::env::var("LOCTREE_PARSER") {
return normalize_parser_strategy(&env);
}
normalize_parser_strategy(&self.analyzer.parser)
}
}
fn normalize_parser_strategy(value: &str) -> &'static str {
match value.trim().to_ascii_lowercase().as_str() {
"ts" | "tree-sitter" | "treesitter" => "ts",
"oxc" | "" => "oxc",
_ => "oxc",
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = LoctreeConfig::default();
assert!(config.tauri.command_macros.is_empty());
assert!(config.tauri.dom_exclusions.is_empty());
assert!(config.tauri.non_invoke_exclusions.is_empty());
assert!(config.tauri.invalid_command_names.is_empty());
assert!(!config.has_custom_command_macros());
}
#[test]
fn test_load_missing_file() {
let temp = TempDir::new().expect("temp dir");
let config = LoctreeConfig::load(temp.path());
assert!(config.tauri.command_macros.is_empty());
}
#[test]
fn test_load_valid_config() {
let temp = TempDir::new().expect("temp dir");
let loctree_dir = temp.path().join(".loctree");
std::fs::create_dir_all(&loctree_dir).expect("create .loctree");
let config_path = loctree_dir.join("config.toml");
let mut file = std::fs::File::create(&config_path).expect("create config");
writeln!(
file,
r#"
event_wrappers = ["emit_compat"]
[tauri]
command_macros = ["api_cmd_tauri", "custom_command"]
dom_exclusions = ["fetch"]
non_invoke_exclusions = ["wrapCommand"]
invalid_command_names = ["npm"]
"#
)
.expect("write config");
let config = LoctreeConfig::load(temp.path());
assert_eq!(config.tauri.command_macros.len(), 2);
assert!(
config
.tauri
.command_macros
.contains(&"api_cmd_tauri".to_string())
);
assert!(
config
.tauri
.command_macros
.contains(&"custom_command".to_string())
);
assert!(config.tauri.dom_exclusions.contains(&"fetch".to_string()));
assert!(
config
.tauri
.non_invoke_exclusions
.contains(&"wrapCommand".to_string())
);
assert!(
config
.tauri
.invalid_command_names
.contains(&"npm".to_string())
);
assert!(config.event_wrappers.contains(&"emit_compat".to_string()));
assert!(config.has_custom_command_macros());
}
#[test]
fn parser_strategy_defaults_to_oxc() {
unsafe {
std::env::remove_var("LOCTREE_PARSER");
}
let config = LoctreeConfig::default();
assert_eq!(config.parser_strategy(), "oxc");
}
#[test]
fn parser_strategy_respects_config_field() {
unsafe {
std::env::remove_var("LOCTREE_PARSER");
}
let temp = TempDir::new().expect("temp dir");
let loctree_dir = temp.path().join(".loctree");
std::fs::create_dir_all(&loctree_dir).expect("create .loctree");
let config_path = loctree_dir.join("config.toml");
let mut file = std::fs::File::create(&config_path).expect("create config");
writeln!(
file,
r#"
[analyzer]
parser = "ts"
"#
)
.expect("write config");
let config = LoctreeConfig::load(temp.path());
assert_eq!(config.parser_strategy(), "ts");
}
#[test]
fn parser_strategy_falls_back_on_unknown_value() {
unsafe {
std::env::remove_var("LOCTREE_PARSER");
}
let mut config = LoctreeConfig::default();
config.analyzer.parser = "swc".to_string();
assert_eq!(config.parser_strategy(), "oxc");
}
#[test]
fn test_load_empty_config() {
let temp = TempDir::new().expect("temp dir");
let loctree_dir = temp.path().join(".loctree");
std::fs::create_dir_all(&loctree_dir).expect("create .loctree");
let config_path = loctree_dir.join("config.toml");
std::fs::File::create(&config_path).expect("create empty config");
let config = LoctreeConfig::load(temp.path());
assert!(config.tauri.command_macros.is_empty());
}
}