Skip to main content

tauri_typegen/build/
mod.rs

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
17/// Build-time code generation orchestrator.
18///
19/// Integrates TypeScript binding generation into Rust build scripts.
20/// This allows automatic regeneration of bindings whenever the Rust code changes.
21pub struct BuildSystem {
22    logger: Logger,
23}
24
25impl BuildSystem {
26    /// Create a new build system instance.
27    ///
28    /// # Arguments
29    ///
30    /// * `verbose` - Enable verbose output
31    /// * `debug` - Enable debug logging
32    pub fn new(verbose: bool, debug: bool) -> Self {
33        Self {
34            logger: Logger::new(verbose, debug),
35        }
36    }
37
38    /// Generate TypeScript bindings at build time.
39    ///
40    /// This is the recommended way to integrate tauri-typegen into your build process.
41    /// Call this from your `src-tauri/build.rs` file to automatically generate bindings
42    /// whenever you run `cargo build` or `cargo tauri dev`.
43    ///
44    /// # Returns
45    ///
46    /// Returns `Ok(())` on success, or an error if generation fails.
47    ///
48    /// # Example
49    ///
50    /// In `src-tauri/build.rs`:
51    ///
52    /// ```rust,ignore
53    /// fn main() {
54    ///     // Generate TypeScript bindings before build
55    ///     tauri_typegen::BuildSystem::generate_at_build_time()
56    ///         .expect("Failed to generate TypeScript bindings");
57    ///
58    ///     tauri_build::build()
59    /// }
60    /// ```
61    ///
62    /// # Configuration
63    ///
64    /// Reads configuration from `tauri.conf.json` in the project root.
65    /// If no configuration is found, uses default settings with vanilla TypeScript output.
66    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    /// Run the complete generation process
72    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        // Try to load from tauri.conf.json first
124        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        // Try standalone config file
144        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        // Use defaults
161        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        // Set up cargo rerun directives
170        println!("cargo:rerun-if-changed={}", config.project_path);
171
172        // Watch for changes in configuration files
173        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        // Watch output directory for cleanup detection
181        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        // Check cache to see if regeneration is needed (unless force is set)
202        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                    // Return list of existing files without regenerating
216                    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                    // If we can't get existing files, fall through to regenerate
221                    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        // Generate dependency visualization if requested
250        if config.should_visualize_deps() {
251            self.generate_dependency_visualization(&analyzer, &commands, &config.output_path)?;
252        }
253
254        // Save cache after successful generation
255        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        // Create the project path directory so validation passes
327        let custom_src_path = temp_dir.path().join("custom-src");
328        std::fs::create_dir_all(&custom_src_path).unwrap();
329
330        // Create a tauri.conf.json with typegen plugin configuration
331        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        // Create a project path that exists for validation
364        let project_path = temp_dir.path().join("src-tauri");
365        std::fs::create_dir_all(&project_path).unwrap();
366
367        // Create a standalone typegen.json configuration
368        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        // Create an invalid tauri.conf.json (no typegen section)
397        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        // Should fall back to defaults
410        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}