tauri_typegen/build/
mod.rs

1pub mod dependency_resolver;
2pub mod output_manager;
3pub mod project_scanner;
4
5use crate::analysis::CommandAnalyzer;
6use crate::generators::create_generator;
7use crate::interface::config::{ConfigError, GenerateConfig};
8use crate::interface::output::{Logger, ProgressReporter};
9use std::path::Path;
10
11pub use dependency_resolver::*;
12pub use output_manager::*;
13pub use project_scanner::*;
14
15/// Build-time code generation orchestrator.
16///
17/// Integrates TypeScript binding generation into Rust build scripts.
18/// This allows automatic regeneration of bindings whenever the Rust code changes.
19pub struct BuildSystem {
20    logger: Logger,
21}
22
23impl BuildSystem {
24    /// Create a new build system instance.
25    ///
26    /// # Arguments
27    ///
28    /// * `verbose` - Enable verbose output
29    /// * `debug` - Enable debug logging
30    pub fn new(verbose: bool, debug: bool) -> Self {
31        Self {
32            logger: Logger::new(verbose, debug),
33        }
34    }
35
36    /// Generate TypeScript bindings at build time.
37    ///
38    /// This is the recommended way to integrate tauri-typegen into your build process.
39    /// Call this from your `src-tauri/build.rs` file to automatically generate bindings
40    /// whenever you run `cargo build` or `cargo tauri dev`.
41    ///
42    /// # Returns
43    ///
44    /// Returns `Ok(())` on success, or an error if generation fails.
45    ///
46    /// # Example
47    ///
48    /// In `src-tauri/build.rs`:
49    ///
50    /// ```rust,ignore
51    /// fn main() {
52    ///     // Generate TypeScript bindings before build
53    ///     tauri_typegen::BuildSystem::generate_at_build_time()
54    ///         .expect("Failed to generate TypeScript bindings");
55    ///
56    ///     tauri_build::build()
57    /// }
58    /// ```
59    ///
60    /// # Configuration
61    ///
62    /// Reads configuration from `tauri.conf.json` in the project root.
63    /// If no configuration is found, uses default settings with vanilla TypeScript output.
64    pub fn generate_at_build_time() -> Result<(), Box<dyn std::error::Error>> {
65        let build_system = Self::new(false, false);
66        build_system.run_generation()
67    }
68
69    /// Run the complete generation process
70    pub fn run_generation(&self) -> Result<(), Box<dyn std::error::Error>> {
71        let mut reporter = ProgressReporter::new(self.logger.clone(), 5);
72
73        reporter.start_step("Detecting Tauri project");
74        let project_scanner = ProjectScanner::new();
75        let project_info = match project_scanner.detect_project()? {
76            Some(info) => {
77                reporter.complete_step(Some(&format!(
78                    "Found project at {}",
79                    info.root_path.display()
80                )));
81                info
82            }
83            None => {
84                reporter.complete_step(Some("No Tauri project detected, skipping generation"));
85                return Ok(());
86            }
87        };
88
89        reporter.start_step("Loading configuration");
90        let config = self.load_configuration(&project_info)?;
91        reporter.complete_step(Some(&format!(
92            "Using {} validation with output to {}",
93            config.validation_library, config.output_path
94        )));
95
96        reporter.start_step("Setting up build dependencies");
97        self.setup_build_dependencies(&config)?;
98        reporter.complete_step(None);
99
100        reporter.start_step("Analyzing and generating bindings");
101        let generated_files = self.generate_bindings(&config)?;
102        reporter.complete_step(Some(&format!("Generated {} files", generated_files.len())));
103
104        reporter.start_step("Managing output");
105        let mut output_manager = OutputManager::new(&config.output_path);
106        output_manager.finalize_generation(&generated_files)?;
107        reporter.complete_step(None);
108
109        reporter.finish(&format!(
110            "Successfully generated TypeScript bindings for {} commands",
111            generated_files.len()
112        ));
113
114        Ok(())
115    }
116
117    fn load_configuration(
118        &self,
119        project_info: &ProjectInfo,
120    ) -> Result<GenerateConfig, ConfigError> {
121        // Try to load from tauri.conf.json first
122        if let Some(tauri_config_path) = &project_info.tauri_config_path {
123            if tauri_config_path.exists() {
124                match GenerateConfig::from_tauri_config(tauri_config_path) {
125                    Ok(Some(config)) => {
126                        self.logger
127                            .debug("Loaded configuration from tauri.conf.json");
128                        return Ok(config);
129                    }
130                    Ok(None) => {}
131                    Err(e) => {
132                        self.logger.warning(&format!(
133                            "Failed to load config from tauri.conf.json: {}. Using defaults.",
134                            e
135                        ));
136                    }
137                }
138            }
139        }
140
141        // Try standalone config file
142        let standalone_config = project_info.root_path.join("typegen.json");
143        if standalone_config.exists() {
144            match GenerateConfig::from_file(&standalone_config) {
145                Ok(config) => {
146                    self.logger.debug("Loaded configuration from typegen.json");
147                    return Ok(config);
148                }
149                Err(e) => {
150                    self.logger.warning(&format!(
151                        "Failed to load config from typegen.json: {}. Using defaults.",
152                        e
153                    ));
154                }
155            }
156        }
157
158        // Use defaults
159        self.logger.debug("Using default configuration");
160        Ok(GenerateConfig::default())
161    }
162
163    fn setup_build_dependencies(
164        &self,
165        config: &GenerateConfig,
166    ) -> Result<(), Box<dyn std::error::Error>> {
167        // Set up cargo rerun directives
168        println!("cargo:rerun-if-changed={}", config.project_path);
169
170        // Watch for changes in configuration files
171        if Path::new("tauri.conf.json").exists() {
172            println!("cargo:rerun-if-changed=tauri.conf.json");
173        }
174        if Path::new("typegen.json").exists() {
175            println!("cargo:rerun-if-changed=typegen.json");
176        }
177
178        // Watch output directory for cleanup detection
179        if Path::new(&config.output_path).exists() {
180            println!("cargo:rerun-if-changed={}", config.output_path);
181        }
182
183        Ok(())
184    }
185
186    fn generate_bindings(
187        &self,
188        config: &GenerateConfig,
189    ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
190        let mut analyzer = CommandAnalyzer::new();
191        let commands = analyzer.analyze_project(&config.project_path)?;
192
193        if commands.is_empty() {
194            self.logger
195                .info("No Tauri commands found. Skipping generation.");
196            return Ok(vec![]);
197        }
198
199        let validation = match config.validation_library.as_str() {
200            "zod" | "none" => Some(config.validation_library.clone()),
201            _ => return Err("Invalid validation library. Use 'zod' or 'none'".into()),
202        };
203
204        let mut generator = create_generator(validation);
205        let generated_files = generator.generate_models(
206            &commands,
207            analyzer.get_discovered_structs(),
208            &config.output_path,
209            &analyzer,
210            config,
211        )?;
212
213        // Generate dependency visualization if requested
214        if config.should_visualize_deps() {
215            self.generate_dependency_visualization(&analyzer, &commands, &config.output_path)?;
216        }
217
218        Ok(generated_files)
219    }
220
221    fn generate_dependency_visualization(
222        &self,
223        analyzer: &CommandAnalyzer,
224        commands: &[crate::models::CommandInfo],
225        output_path: &str,
226    ) -> Result<(), Box<dyn std::error::Error>> {
227        use std::fs;
228
229        self.logger.debug("Generating dependency visualization");
230
231        let text_viz = analyzer.visualize_dependencies(commands);
232        let viz_file_path = Path::new(output_path).join("dependency-graph.txt");
233        fs::write(&viz_file_path, text_viz)?;
234
235        let dot_viz = analyzer.generate_dot_graph(commands);
236        let dot_file_path = Path::new(output_path).join("dependency-graph.dot");
237        fs::write(&dot_file_path, dot_viz)?;
238
239        self.logger.verbose(&format!(
240            "Generated dependency graphs: {} and {}",
241            viz_file_path.display(),
242            dot_file_path.display()
243        ));
244
245        Ok(())
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use tempfile::TempDir;
253
254    #[test]
255    fn test_build_system_creation() {
256        let build_system = BuildSystem::new(true, false);
257        assert!(build_system
258            .logger
259            .should_log(crate::interface::output::LogLevel::Verbose));
260    }
261
262    #[test]
263    fn test_load_default_configuration() {
264        let temp_dir = TempDir::new().unwrap();
265        let project_info = ProjectInfo {
266            root_path: temp_dir.path().to_path_buf(),
267            src_tauri_path: temp_dir.path().join("src-tauri"),
268            tauri_config_path: None,
269        };
270
271        let build_system = BuildSystem::new(false, false);
272        let config = build_system.load_configuration(&project_info).unwrap();
273
274        assert_eq!(config.validation_library, "none");
275        assert_eq!(config.project_path, "./src-tauri");
276    }
277}