1pub mod dependency_resolver;
2pub mod generation_cache;
3pub mod output_manager;
4pub mod project_scanner;
5
6use crate::analysis::CommandAnalyzer;
7use crate::generators::create_generator;
8use crate::interface::config::{ConfigError, GenerateConfig};
9use crate::interface::output::{Logger, ProgressReporter};
10use std::path::Path;
11
12pub use dependency_resolver::*;
13pub use generation_cache::*;
14pub use output_manager::*;
15pub use project_scanner::*;
16
17pub struct BuildSystem {
22 logger: Logger,
23}
24
25impl BuildSystem {
26 pub fn new(verbose: bool, debug: bool) -> Self {
33 Self {
34 logger: Logger::new(verbose, debug),
35 }
36 }
37
38 pub fn generate_at_build_time() -> Result<(), Box<dyn std::error::Error>> {
67 let build_system = Self::new(false, false);
68 build_system.run_generation()
69 }
70
71 pub fn run_generation(&self) -> Result<(), Box<dyn std::error::Error>> {
73 let mut reporter = ProgressReporter::new(self.logger.clone(), 5);
74
75 reporter.start_step("Detecting Tauri project");
76 let project_scanner = ProjectScanner::new();
77 let project_info = match project_scanner.detect_project()? {
78 Some(info) => {
79 reporter.complete_step(Some(&format!(
80 "Found project at {}",
81 info.root_path.display()
82 )));
83 info
84 }
85 None => {
86 reporter.complete_step(Some("No Tauri project detected, skipping generation"));
87 return Ok(());
88 }
89 };
90
91 reporter.start_step("Loading configuration");
92 let config = self.load_configuration(&project_info)?;
93 reporter.complete_step(Some(&format!(
94 "Using {} validation with output to {}",
95 config.validation_library, config.output_path
96 )));
97
98 reporter.start_step("Setting up build dependencies");
99 self.setup_build_dependencies(&config)?;
100 reporter.complete_step(None);
101
102 reporter.start_step("Analyzing and generating bindings");
103 let generated_files = self.generate_bindings(&config)?;
104 reporter.complete_step(Some(&format!("Generated {} files", generated_files.len())));
105
106 reporter.start_step("Managing output");
107 let mut output_manager = OutputManager::new(&config.output_path);
108 output_manager.finalize_generation(&generated_files)?;
109 reporter.complete_step(None);
110
111 reporter.finish(&format!(
112 "Successfully generated TypeScript bindings for {} commands",
113 generated_files.len()
114 ));
115
116 Ok(())
117 }
118
119 fn load_configuration(
120 &self,
121 project_info: &ProjectInfo,
122 ) -> Result<GenerateConfig, ConfigError> {
123 if let Some(tauri_config_path) = &project_info.tauri_config_path {
125 if tauri_config_path.exists() {
126 match GenerateConfig::from_tauri_config(tauri_config_path) {
127 Ok(Some(config)) => {
128 self.logger
129 .debug("Loaded configuration from tauri.conf.json");
130 return Ok(config);
131 }
132 Ok(None) => {}
133 Err(e) => {
134 self.logger.warning(&format!(
135 "Failed to load config from tauri.conf.json: {}. Using defaults.",
136 e
137 ));
138 }
139 }
140 }
141 }
142
143 let standalone_config = project_info.root_path.join("typegen.json");
145 if standalone_config.exists() {
146 match GenerateConfig::from_file(&standalone_config) {
147 Ok(config) => {
148 self.logger.debug("Loaded configuration from typegen.json");
149 return Ok(config);
150 }
151 Err(e) => {
152 self.logger.warning(&format!(
153 "Failed to load config from typegen.json: {}. Using defaults.",
154 e
155 ));
156 }
157 }
158 }
159
160 self.logger.debug("Using default configuration");
162 Ok(GenerateConfig::default())
163 }
164
165 fn setup_build_dependencies(
166 &self,
167 config: &GenerateConfig,
168 ) -> Result<(), Box<dyn std::error::Error>> {
169 println!("cargo:rerun-if-changed={}", config.project_path);
171
172 if Path::new("tauri.conf.json").exists() {
174 println!("cargo:rerun-if-changed=tauri.conf.json");
175 }
176 if Path::new("typegen.json").exists() {
177 println!("cargo:rerun-if-changed=typegen.json");
178 }
179
180 if Path::new(&config.output_path).exists() {
182 println!("cargo:rerun-if-changed={}", config.output_path);
183 }
184
185 Ok(())
186 }
187
188 fn generate_bindings(
189 &self,
190 config: &GenerateConfig,
191 ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
192 let mut analyzer = CommandAnalyzer::new();
193 let commands = analyzer.analyze_project(&config.project_path)?;
194
195 if commands.is_empty() {
196 self.logger
197 .info("No Tauri commands found. Skipping generation.");
198 return Ok(vec![]);
199 }
200
201 let discovered_structs = analyzer.get_discovered_structs();
203 if config.should_force() {
204 self.logger.verbose("Force flag set, regenerating bindings");
205 } else {
206 match GenerationCache::needs_regeneration(
207 &config.output_path,
208 &commands,
209 discovered_structs,
210 config,
211 ) {
212 Ok(false) => {
213 self.logger
214 .verbose("Cache hit - no changes detected, skipping generation");
215 let output_manager = OutputManager::new(&config.output_path);
217 if let Ok(metadata) = output_manager.get_generation_metadata() {
218 return Ok(metadata.files.iter().map(|f| f.name.clone()).collect());
219 }
220 self.logger
222 .debug("Could not get existing file list, regenerating");
223 }
224 Ok(true) => {
225 self.logger
226 .verbose("Cache miss - changes detected, regenerating");
227 }
228 Err(e) => {
229 self.logger
230 .debug(&format!("Cache check failed: {}, regenerating", e));
231 }
232 }
233 }
234
235 let validation = match config.validation_library.as_str() {
236 "zod" | "none" => Some(config.validation_library.clone()),
237 _ => return Err("Invalid validation library. Use 'zod' or 'none'".into()),
238 };
239
240 let mut generator = create_generator(validation);
241 let generated_files = generator.generate_models(
242 &commands,
243 discovered_structs,
244 &config.output_path,
245 &analyzer,
246 config,
247 )?;
248
249 if config.should_visualize_deps() {
251 self.generate_dependency_visualization(&analyzer, &commands, &config.output_path)?;
252 }
253
254 let cache = GenerationCache::new(&commands, discovered_structs, config)?;
256 if let Err(e) = cache.save(&config.output_path) {
257 self.logger
258 .warning(&format!("Failed to save generation cache: {}", e));
259 }
260
261 Ok(generated_files)
262 }
263
264 fn generate_dependency_visualization(
265 &self,
266 analyzer: &CommandAnalyzer,
267 commands: &[crate::models::CommandInfo],
268 output_path: &str,
269 ) -> Result<(), Box<dyn std::error::Error>> {
270 use std::fs;
271
272 self.logger.debug("Generating dependency visualization");
273
274 let text_viz = analyzer.visualize_dependencies(commands);
275 let viz_file_path = Path::new(output_path).join("dependency-graph.txt");
276 fs::write(&viz_file_path, text_viz)?;
277
278 let dot_viz = analyzer.generate_dot_graph(commands);
279 let dot_file_path = Path::new(output_path).join("dependency-graph.dot");
280 fs::write(&dot_file_path, dot_viz)?;
281
282 self.logger.verbose(&format!(
283 "Generated dependency graphs: {} and {}",
284 viz_file_path.display(),
285 dot_file_path.display()
286 ));
287
288 Ok(())
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use tempfile::TempDir;
296
297 #[test]
298 fn test_build_system_creation() {
299 let build_system = BuildSystem::new(true, false);
300 assert!(build_system
301 .logger
302 .should_log(crate::interface::output::LogLevel::Verbose));
303 }
304
305 #[test]
306 fn test_load_default_configuration() {
307 let temp_dir = TempDir::new().unwrap();
308 let project_info = ProjectInfo {
309 root_path: temp_dir.path().to_path_buf(),
310 src_tauri_path: temp_dir.path().join("src-tauri"),
311 tauri_config_path: None,
312 };
313
314 let build_system = BuildSystem::new(false, false);
315 let config = build_system.load_configuration(&project_info).unwrap();
316
317 assert_eq!(config.validation_library, "none");
318 assert_eq!(config.project_path, "./src-tauri");
319 }
320
321 #[test]
322 fn test_load_configuration_from_tauri_config() {
323 let temp_dir = TempDir::new().unwrap();
324 let tauri_config_path = temp_dir.path().join("tauri.conf.json");
325
326 let custom_src_path = temp_dir.path().join("custom-src");
328 std::fs::create_dir_all(&custom_src_path).unwrap();
329
330 let config_content = format!(
332 r#"{{
333 "plugins": {{
334 "typegen": {{
335 "projectPath": "{}",
336 "outputPath": "./custom-output",
337 "validationLibrary": "zod"
338 }}
339 }}
340 }}"#,
341 custom_src_path.to_string_lossy()
342 );
343 std::fs::write(&tauri_config_path, &config_content).unwrap();
344
345 let project_info = ProjectInfo {
346 root_path: temp_dir.path().to_path_buf(),
347 src_tauri_path: temp_dir.path().join("src-tauri"),
348 tauri_config_path: Some(tauri_config_path),
349 };
350
351 let build_system = BuildSystem::new(false, false);
352 let config = build_system.load_configuration(&project_info).unwrap();
353
354 assert_eq!(config.validation_library, "zod");
355 assert_eq!(config.output_path, "./custom-output");
356 }
357
358 #[test]
359 fn test_load_configuration_from_standalone_file() {
360 let temp_dir = TempDir::new().unwrap();
361 let typegen_config_path = temp_dir.path().join("typegen.json");
362
363 let project_path = temp_dir.path().join("src-tauri");
365 std::fs::create_dir_all(&project_path).unwrap();
366
367 let config_content = format!(
369 r#"{{
370 "project_path": "{}",
371 "output_path": "./standalone-output",
372 "validation_library": "zod"
373 }}"#,
374 project_path.to_string_lossy()
375 );
376 std::fs::write(&typegen_config_path, config_content).unwrap();
377
378 let project_info = ProjectInfo {
379 root_path: temp_dir.path().to_path_buf(),
380 src_tauri_path: project_path.clone(),
381 tauri_config_path: None,
382 };
383
384 let build_system = BuildSystem::new(false, false);
385 let config = build_system.load_configuration(&project_info).unwrap();
386
387 assert_eq!(config.validation_library, "zod");
388 assert_eq!(config.output_path, "./standalone-output");
389 }
390
391 #[test]
392 fn test_load_configuration_falls_back_on_invalid_tauri_config() {
393 let temp_dir = TempDir::new().unwrap();
394 let tauri_config_path = temp_dir.path().join("tauri.conf.json");
395
396 let config_content = r#"{"build": {}}"#;
398 std::fs::write(&tauri_config_path, config_content).unwrap();
399
400 let project_info = ProjectInfo {
401 root_path: temp_dir.path().to_path_buf(),
402 src_tauri_path: temp_dir.path().join("src-tauri"),
403 tauri_config_path: Some(tauri_config_path),
404 };
405
406 let build_system = BuildSystem::new(false, false);
407 let config = build_system.load_configuration(&project_info).unwrap();
408
409 assert_eq!(config.validation_library, "none");
411 assert_eq!(config.project_path, "./src-tauri");
412 }
413
414 #[test]
415 fn test_build_system_with_verbose_logging() {
416 let build_system = BuildSystem::new(true, true);
417 assert!(build_system
418 .logger
419 .should_log(crate::interface::output::LogLevel::Verbose));
420 assert!(build_system
421 .logger
422 .should_log(crate::interface::output::LogLevel::Debug));
423 }
424
425 #[test]
426 fn test_build_system_without_verbose_logging() {
427 let build_system = BuildSystem::new(false, false);
428 assert!(!build_system
429 .logger
430 .should_log(crate::interface::output::LogLevel::Verbose));
431 assert!(!build_system
432 .logger
433 .should_log(crate::interface::output::LogLevel::Debug));
434 }
435}