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::generator::BindingsGenerator;
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
16pub struct BuildSystem {
17    logger: Logger,
18}
19
20impl BuildSystem {
21    pub fn new(verbose: bool, debug: bool) -> Self {
22        Self {
23            logger: Logger::new(verbose, debug),
24        }
25    }
26
27    /// Generate TypeScript bindings at build time
28    pub fn generate_at_build_time() -> Result<(), Box<dyn std::error::Error>> {
29        let build_system = Self::new(false, false);
30        build_system.run_generation()
31    }
32
33    /// Run the complete generation process
34    pub fn run_generation(&self) -> Result<(), Box<dyn std::error::Error>> {
35        let mut reporter = ProgressReporter::new(self.logger.clone(), 5);
36
37        reporter.start_step("Detecting Tauri project");
38        let project_scanner = ProjectScanner::new();
39        let project_info = match project_scanner.detect_project()? {
40            Some(info) => {
41                reporter.complete_step(Some(&format!(
42                    "Found project at {}",
43                    info.root_path.display()
44                )));
45                info
46            }
47            None => {
48                reporter.complete_step(Some("No Tauri project detected, skipping generation"));
49                return Ok(());
50            }
51        };
52
53        reporter.start_step("Loading configuration");
54        let config = self.load_configuration(&project_info)?;
55        reporter.complete_step(Some(&format!(
56            "Using {} validation with output to {}",
57            config.validation_library, config.output_path
58        )));
59
60        reporter.start_step("Setting up build dependencies");
61        self.setup_build_dependencies(&config)?;
62        reporter.complete_step(None);
63
64        reporter.start_step("Analyzing and generating bindings");
65        let generated_files = self.generate_bindings(&config)?;
66        reporter.complete_step(Some(&format!("Generated {} files", generated_files.len())));
67
68        reporter.start_step("Managing output");
69        let mut output_manager = OutputManager::new(&config.output_path);
70        output_manager.finalize_generation(&generated_files)?;
71        reporter.complete_step(None);
72
73        reporter.finish(&format!(
74            "Successfully generated TypeScript bindings for {} commands",
75            generated_files.len()
76        ));
77
78        Ok(())
79    }
80
81    fn load_configuration(
82        &self,
83        project_info: &ProjectInfo,
84    ) -> Result<GenerateConfig, ConfigError> {
85        // Try to load from tauri.conf.json first
86        if let Some(tauri_config_path) = &project_info.tauri_config_path {
87            if tauri_config_path.exists() {
88                match GenerateConfig::from_tauri_config(tauri_config_path) {
89                    Ok(Some(config)) => {
90                        self.logger
91                            .debug("Loaded configuration from tauri.conf.json");
92                        return Ok(config);
93                    }
94                    Ok(None) => {}
95                    Err(e) => {
96                        self.logger.warning(&format!(
97                            "Failed to load config from tauri.conf.json: {}. Using defaults.",
98                            e
99                        ));
100                    }
101                }
102            }
103        }
104
105        // Try standalone config file
106        let standalone_config = project_info.root_path.join("typegen.json");
107        if standalone_config.exists() {
108            match GenerateConfig::from_file(&standalone_config) {
109                Ok(config) => {
110                    self.logger.debug("Loaded configuration from typegen.json");
111                    return Ok(config);
112                }
113                Err(e) => {
114                    self.logger.warning(&format!(
115                        "Failed to load config from typegen.json: {}. Using defaults.",
116                        e
117                    ));
118                }
119            }
120        }
121
122        // Use defaults
123        self.logger.debug("Using default configuration");
124        Ok(GenerateConfig::default())
125    }
126
127    fn setup_build_dependencies(
128        &self,
129        config: &GenerateConfig,
130    ) -> Result<(), Box<dyn std::error::Error>> {
131        // Set up cargo rerun directives
132        println!("cargo:rerun-if-changed={}", config.project_path);
133
134        // Watch for changes in configuration files
135        if Path::new("tauri.conf.json").exists() {
136            println!("cargo:rerun-if-changed=tauri.conf.json");
137        }
138        if Path::new("typegen.json").exists() {
139            println!("cargo:rerun-if-changed=typegen.json");
140        }
141
142        // Watch output directory for cleanup detection
143        if Path::new(&config.output_path).exists() {
144            println!("cargo:rerun-if-changed={}", config.output_path);
145        }
146
147        Ok(())
148    }
149
150    fn generate_bindings(
151        &self,
152        config: &GenerateConfig,
153    ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
154        let mut analyzer = CommandAnalyzer::new();
155        let commands = analyzer.analyze_project(&config.project_path)?;
156
157        if commands.is_empty() {
158            self.logger
159                .info("No Tauri commands found. Skipping generation.");
160            return Ok(vec![]);
161        }
162
163        let validation = match config.validation_library.as_str() {
164            "zod" | "none" => Some(config.validation_library.clone()),
165            _ => return Err("Invalid validation library. Use 'zod' or 'none'".into()),
166        };
167
168        let mut generator = BindingsGenerator::new(validation);
169        let generated_files = generator.generate_models(
170            &commands,
171            analyzer.get_discovered_structs(),
172            &config.output_path,
173            &analyzer,
174        )?;
175
176        // Generate dependency visualization if requested
177        if config.should_visualize_deps() {
178            self.generate_dependency_visualization(&analyzer, &commands, &config.output_path)?;
179        }
180
181        Ok(generated_files)
182    }
183
184    fn generate_dependency_visualization(
185        &self,
186        analyzer: &CommandAnalyzer,
187        commands: &[crate::models::CommandInfo],
188        output_path: &str,
189    ) -> Result<(), Box<dyn std::error::Error>> {
190        use std::fs;
191
192        self.logger.debug("Generating dependency visualization");
193
194        let text_viz = analyzer.visualize_dependencies(commands);
195        let viz_file_path = Path::new(output_path).join("dependency-graph.txt");
196        fs::write(&viz_file_path, text_viz)?;
197
198        let dot_viz = analyzer.generate_dot_graph(commands);
199        let dot_file_path = Path::new(output_path).join("dependency-graph.dot");
200        fs::write(&dot_file_path, dot_viz)?;
201
202        self.logger.verbose(&format!(
203            "Generated dependency graphs: {} and {}",
204            viz_file_path.display(),
205            dot_file_path.display()
206        ));
207
208        Ok(())
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use tempfile::TempDir;
216
217    #[test]
218    fn test_build_system_creation() {
219        let build_system = BuildSystem::new(true, false);
220        assert!(build_system
221            .logger
222            .should_log(crate::interface::output::LogLevel::Verbose));
223    }
224
225    #[test]
226    fn test_load_default_configuration() {
227        let temp_dir = TempDir::new().unwrap();
228        let project_info = ProjectInfo {
229            root_path: temp_dir.path().to_path_buf(),
230            src_tauri_path: temp_dir.path().join("src-tauri"),
231            tauri_config_path: None,
232        };
233
234        let build_system = BuildSystem::new(false, false);
235        let config = build_system.load_configuration(&project_info).unwrap();
236
237        assert_eq!(config.validation_library, "none");
238        assert_eq!(config.project_path, "./src-tauri");
239    }
240}