tauri_typegen/build/
mod.rs1pub 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
15pub struct BuildSystem {
20 logger: Logger,
21}
22
23impl BuildSystem {
24 pub fn new(verbose: bool, debug: bool) -> Self {
31 Self {
32 logger: Logger::new(verbose, debug),
33 }
34 }
35
36 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 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 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 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 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 println!("cargo:rerun-if-changed={}", config.project_path);
169
170 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 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 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}