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        let discovered_events = analyzer.get_discovered_events();
204        if config.should_force() {
205            self.logger.verbose("Force flag set, regenerating bindings");
206        } else {
207            match GenerationCache::needs_regeneration(
208                &config.output_path,
209                &commands,
210                discovered_structs,
211                discovered_events,
212                config,
213            ) {
214                Ok(false) => {
215                    self.logger
216                        .verbose("Cache hit - no changes detected, skipping generation");
217                    // Return list of existing files without regenerating
218                    let output_manager = OutputManager::new(&config.output_path);
219                    if let Ok(metadata) = output_manager.get_generation_metadata() {
220                        return Ok(metadata.files.iter().map(|f| f.name.clone()).collect());
221                    }
222                    // If we can't get existing files, fall through to regenerate
223                    self.logger
224                        .debug("Could not get existing file list, regenerating");
225                }
226                Ok(true) => {
227                    self.logger
228                        .verbose("Cache miss - changes detected, regenerating");
229                }
230                Err(e) => {
231                    self.logger
232                        .debug(&format!("Cache check failed: {}, regenerating", e));
233                }
234            }
235        }
236
237        let validation = match config.validation_library.as_str() {
238            "zod" | "none" => Some(config.validation_library.clone()),
239            _ => return Err("Invalid validation library. Use 'zod' or 'none'".into()),
240        };
241
242        let mut generator = create_generator(validation);
243        let generated_files = generator.generate_models(
244            &commands,
245            discovered_structs,
246            &config.output_path,
247            &analyzer,
248            config,
249        )?;
250
251        // Generate dependency visualization if requested
252        if config.should_visualize_deps() {
253            self.generate_dependency_visualization(&analyzer, &commands, &config.output_path)?;
254        }
255
256        // Save cache after successful generation
257        let cache = GenerationCache::new(&commands, discovered_structs, discovered_events, config)?;
258        if let Err(e) = cache.save(&config.output_path) {
259            self.logger
260                .warning(&format!("Failed to save generation cache: {}", e));
261        }
262
263        Ok(generated_files)
264    }
265
266    fn generate_dependency_visualization(
267        &self,
268        analyzer: &CommandAnalyzer,
269        commands: &[crate::models::CommandInfo],
270        output_path: &str,
271    ) -> Result<(), Box<dyn std::error::Error>> {
272        use std::fs;
273
274        self.logger.debug("Generating dependency visualization");
275
276        let text_viz = analyzer.visualize_dependencies(commands);
277        let viz_file_path = Path::new(output_path).join("dependency-graph.txt");
278        fs::write(&viz_file_path, text_viz)?;
279
280        let dot_viz = analyzer.generate_dot_graph(commands);
281        let dot_file_path = Path::new(output_path).join("dependency-graph.dot");
282        fs::write(&dot_file_path, dot_viz)?;
283
284        self.logger.verbose(&format!(
285            "Generated dependency graphs: {} and {}",
286            viz_file_path.display(),
287            dot_file_path.display()
288        ));
289
290        Ok(())
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::interface::config::GenerateConfig;
298    use std::path::Path;
299    use tempfile::TempDir;
300
301    fn create_build_config(project_path: &Path, output_path: &Path) -> GenerateConfig {
302        GenerateConfig {
303            project_path: project_path.to_string_lossy().to_string(),
304            output_path: output_path.to_string_lossy().to_string(),
305            validation_library: "none".to_string(),
306            verbose: Some(false),
307            visualize_deps: Some(false),
308            include_private: Some(false),
309            type_mappings: None,
310            exclude_patterns: None,
311            include_patterns: None,
312            default_parameter_case: "camelCase".to_string(),
313            default_field_case: "snake_case".to_string(),
314            force: Some(false),
315        }
316    }
317
318    fn run_generation(build_system: &BuildSystem, config: &GenerateConfig) -> Vec<String> {
319        let generated_files = build_system.generate_bindings(config).unwrap();
320        let mut output_manager = OutputManager::new(&config.output_path);
321        output_manager
322            .finalize_generation(&generated_files)
323            .unwrap();
324        generated_files
325    }
326
327    fn read_generated(output_path: &Path, file_name: &str) -> String {
328        std::fs::read_to_string(output_path.join(file_name)).unwrap()
329    }
330
331    #[test]
332    fn test_build_system_creation() {
333        let build_system = BuildSystem::new(true, false);
334        assert!(build_system
335            .logger
336            .should_log(crate::interface::output::LogLevel::Verbose));
337    }
338
339    #[test]
340    fn test_load_default_configuration() {
341        let temp_dir = TempDir::new().unwrap();
342        let project_info = ProjectInfo {
343            root_path: temp_dir.path().to_path_buf(),
344            src_tauri_path: temp_dir.path().join("src-tauri"),
345            tauri_config_path: None,
346        };
347
348        let build_system = BuildSystem::new(false, false);
349        let config = build_system.load_configuration(&project_info).unwrap();
350
351        assert_eq!(config.validation_library, "none");
352        assert_eq!(config.project_path, "./src-tauri");
353    }
354
355    #[test]
356    fn test_load_configuration_from_tauri_config() {
357        let temp_dir = TempDir::new().unwrap();
358        let tauri_config_path = temp_dir.path().join("tauri.conf.json");
359
360        // Create the project path directory so validation passes
361        let custom_src_path = temp_dir.path().join("custom-src");
362        std::fs::create_dir_all(&custom_src_path).unwrap();
363
364        // Create a tauri.conf.json with typegen plugin configuration
365        let config_content = serde_json::json!({
366            "plugins": {
367                "typegen": {
368                    "projectPath": custom_src_path.to_string_lossy().to_string(),
369                    "outputPath": "./custom-output",
370                    "validationLibrary": "zod"
371                }
372            }
373        })
374        .to_string();
375        std::fs::write(&tauri_config_path, &config_content).unwrap();
376
377        let project_info = ProjectInfo {
378            root_path: temp_dir.path().to_path_buf(),
379            src_tauri_path: temp_dir.path().join("src-tauri"),
380            tauri_config_path: Some(tauri_config_path),
381        };
382
383        let build_system = BuildSystem::new(false, false);
384        let config = build_system.load_configuration(&project_info).unwrap();
385
386        assert_eq!(config.validation_library, "zod");
387        assert_eq!(config.output_path, "./custom-output");
388    }
389
390    #[test]
391    fn test_load_configuration_from_standalone_file() {
392        let temp_dir = TempDir::new().unwrap();
393        let typegen_config_path = temp_dir.path().join("typegen.json");
394
395        // Create a project path that exists for validation
396        let project_path = temp_dir.path().join("src-tauri");
397        std::fs::create_dir_all(&project_path).unwrap();
398
399        // Create a standalone typegen.json configuration
400        let config_content = serde_json::json!({
401            "project_path": project_path.to_string_lossy().to_string(),
402            "output_path": "./standalone-output",
403            "validation_library": "zod"
404        })
405        .to_string();
406        std::fs::write(&typegen_config_path, config_content).unwrap();
407
408        let project_info = ProjectInfo {
409            root_path: temp_dir.path().to_path_buf(),
410            src_tauri_path: project_path.clone(),
411            tauri_config_path: None,
412        };
413
414        let build_system = BuildSystem::new(false, false);
415        let config = build_system.load_configuration(&project_info).unwrap();
416
417        assert_eq!(config.validation_library, "zod");
418        assert_eq!(config.output_path, "./standalone-output");
419    }
420
421    #[test]
422    fn test_load_configuration_falls_back_on_invalid_tauri_config() {
423        let temp_dir = TempDir::new().unwrap();
424        let tauri_config_path = temp_dir.path().join("tauri.conf.json");
425
426        // Create an invalid tauri.conf.json (no typegen section)
427        let config_content = r#"{"build": {}}"#;
428        std::fs::write(&tauri_config_path, config_content).unwrap();
429
430        let project_info = ProjectInfo {
431            root_path: temp_dir.path().to_path_buf(),
432            src_tauri_path: temp_dir.path().join("src-tauri"),
433            tauri_config_path: Some(tauri_config_path),
434        };
435
436        let build_system = BuildSystem::new(false, false);
437        let config = build_system.load_configuration(&project_info).unwrap();
438
439        // Should fall back to defaults
440        assert_eq!(config.validation_library, "none");
441        assert_eq!(config.project_path, "./src-tauri");
442    }
443
444    #[test]
445    fn test_build_system_with_verbose_logging() {
446        let build_system = BuildSystem::new(true, true);
447        assert!(build_system
448            .logger
449            .should_log(crate::interface::output::LogLevel::Verbose));
450        assert!(build_system
451            .logger
452            .should_log(crate::interface::output::LogLevel::Debug));
453    }
454
455    #[test]
456    fn test_build_system_without_verbose_logging() {
457        let build_system = BuildSystem::new(false, false);
458        assert!(!build_system
459            .logger
460            .should_log(crate::interface::output::LogLevel::Verbose));
461        assert!(!build_system
462            .logger
463            .should_log(crate::interface::output::LogLevel::Debug));
464    }
465
466    #[test]
467    fn test_generate_bindings_skips_unrelated_rust_changes() {
468        let temp_dir = TempDir::new().unwrap();
469        let project_path = temp_dir.path().join("src-tauri");
470        let output_path = temp_dir.path().join("generated");
471        std::fs::create_dir_all(&project_path).unwrap();
472
473        let source_file = project_path.join("main.rs");
474        std::fs::write(
475            &source_file,
476            r#"
477            use serde::{Deserialize, Serialize};
478            use tauri::Manager;
479
480            #[derive(Debug, Clone, Serialize, Deserialize)]
481            pub struct Payload {
482                pub value: String,
483            }
484
485            fn helper_text() -> &'static str {
486                "one"
487            }
488
489            #[tauri::command]
490            pub fn fetch_payload() -> Result<Payload, String> {
491                Ok(Payload {
492                    value: helper_text().to_string(),
493                })
494            }
495
496            #[tauri::command]
497            pub fn emit_event(app: tauri::AppHandle) -> Result<(), String> {
498                app.emit("stable-event", Payload {
499                    value: helper_text().to_string(),
500                }).ok();
501                Ok(())
502            }
503        "#,
504        )
505        .unwrap();
506
507        let config = create_build_config(&project_path, &output_path);
508        let build_system = BuildSystem::new(false, false);
509
510        run_generation(&build_system, &config);
511
512        let commands_before = read_generated(&output_path, "commands.ts");
513        let types_before = read_generated(&output_path, "types.ts");
514        let events_before = read_generated(&output_path, "events.ts");
515        let index_before = read_generated(&output_path, "index.ts");
516
517        std::fs::write(
518            &source_file,
519            r#"
520            use serde::{Deserialize, Serialize};
521            use tauri::Manager;
522
523            #[derive(Debug, Clone, Serialize, Deserialize)]
524            pub struct Payload {
525                pub value: String,
526            }
527
528            fn helper_text() -> &'static str {
529                "two"
530            }
531
532            #[tauri::command]
533            pub fn fetch_payload() -> Result<Payload, String> {
534                Ok(Payload {
535                    value: helper_text().to_string(),
536                })
537            }
538
539            #[tauri::command]
540            pub fn emit_event(app: tauri::AppHandle) -> Result<(), String> {
541                app.emit("stable-event", Payload {
542                    value: helper_text().to_string(),
543                }).ok();
544                Ok(())
545            }
546        "#,
547        )
548        .unwrap();
549
550        run_generation(&build_system, &config);
551
552        assert_eq!(commands_before, read_generated(&output_path, "commands.ts"));
553        assert_eq!(types_before, read_generated(&output_path, "types.ts"));
554        assert_eq!(events_before, read_generated(&output_path, "events.ts"));
555        assert_eq!(index_before, read_generated(&output_path, "index.ts"));
556    }
557
558    #[test]
559    fn test_generate_bindings_skips_source_location_only_changes() {
560        let temp_dir = TempDir::new().unwrap();
561        let project_path = temp_dir.path().join("src-tauri");
562        let output_path = temp_dir.path().join("generated");
563        std::fs::create_dir_all(&project_path).unwrap();
564
565        let source_file = project_path.join("main.rs");
566        std::fs::write(
567            &source_file,
568            r#"
569            use serde::{Deserialize, Serialize};
570            use tauri::Manager;
571
572            #[derive(Debug, Clone, Serialize, Deserialize)]
573            pub struct Payload {
574                pub value: String,
575            }
576
577            #[tauri::command]
578            pub fn fetch_payload() -> Result<Payload, String> {
579                Ok(Payload {
580                    value: "one".to_string(),
581                })
582            }
583
584            #[tauri::command]
585            pub fn emit_event(app: tauri::AppHandle) -> Result<(), String> {
586                app.emit("stable-event", Payload {
587                    value: "one".to_string(),
588                }).ok();
589                Ok(())
590            }
591        "#,
592        )
593        .unwrap();
594
595        let config = create_build_config(&project_path, &output_path);
596        let build_system = BuildSystem::new(false, false);
597
598        run_generation(&build_system, &config);
599
600        let commands_before = read_generated(&output_path, "commands.ts");
601        let types_before = read_generated(&output_path, "types.ts");
602        let events_before = read_generated(&output_path, "events.ts");
603
604        std::fs::write(
605            &source_file,
606            r#"
607            use serde::{Deserialize, Serialize};
608            use tauri::Manager;
609
610            // Unrelated comment that shifts every discovered item downward.
611            // The generated bindings should stay byte-stable.
612
613            #[derive(Debug, Clone, Serialize, Deserialize)]
614            pub struct Payload {
615                pub value: String,
616            }
617
618            #[tauri::command]
619            pub fn fetch_payload() -> Result<Payload, String> {
620                Ok(Payload {
621                    value: "one".to_string(),
622                })
623            }
624
625            #[tauri::command]
626            pub fn emit_event(app: tauri::AppHandle) -> Result<(), String> {
627                app.emit("stable-event", Payload {
628                    value: "one".to_string(),
629                }).ok();
630                Ok(())
631            }
632        "#,
633        )
634        .unwrap();
635
636        run_generation(&build_system, &config);
637
638        assert_eq!(commands_before, read_generated(&output_path, "commands.ts"));
639        assert_eq!(types_before, read_generated(&output_path, "types.ts"));
640        assert_eq!(events_before, read_generated(&output_path, "events.ts"));
641    }
642
643    #[test]
644    fn test_generate_bindings_regenerates_when_commands_change() {
645        let temp_dir = TempDir::new().unwrap();
646        let project_path = temp_dir.path().join("src-tauri");
647        let output_path = temp_dir.path().join("generated");
648        std::fs::create_dir_all(&project_path).unwrap();
649
650        let source_file = project_path.join("main.rs");
651        std::fs::write(
652            &source_file,
653            r#"
654            #[tauri::command]
655            pub fn first_command() -> Result<String, String> {
656                Ok("one".to_string())
657            }
658        "#,
659        )
660        .unwrap();
661
662        let config = create_build_config(&project_path, &output_path);
663        let build_system = BuildSystem::new(false, false);
664
665        run_generation(&build_system, &config);
666        let commands_before = read_generated(&output_path, "commands.ts");
667
668        std::fs::write(
669            &source_file,
670            r#"
671            #[tauri::command]
672            pub fn second_command() -> Result<String, String> {
673                Ok("two".to_string())
674            }
675        "#,
676        )
677        .unwrap();
678
679        run_generation(&build_system, &config);
680        let commands_after = read_generated(&output_path, "commands.ts");
681
682        assert_ne!(commands_before, commands_after);
683        assert!(commands_after.contains("secondCommand"));
684        assert!(!commands_after.contains("firstCommand"));
685    }
686
687    #[test]
688    fn test_generate_bindings_regenerates_when_structs_change() {
689        let temp_dir = TempDir::new().unwrap();
690        let project_path = temp_dir.path().join("src-tauri");
691        let output_path = temp_dir.path().join("generated");
692        std::fs::create_dir_all(&project_path).unwrap();
693
694        let source_file = project_path.join("main.rs");
695        std::fs::write(
696            &source_file,
697            r#"
698            use serde::{Deserialize, Serialize};
699
700            #[derive(Debug, Clone, Serialize, Deserialize)]
701            pub struct Payload {
702                pub value: String,
703            }
704
705            #[tauri::command]
706            pub fn fetch_payload() -> Result<Payload, String> {
707                Ok(Payload {
708                    value: "one".to_string(),
709                })
710            }
711        "#,
712        )
713        .unwrap();
714
715        let config = create_build_config(&project_path, &output_path);
716        let build_system = BuildSystem::new(false, false);
717
718        run_generation(&build_system, &config);
719        let types_before = read_generated(&output_path, "types.ts");
720
721        std::fs::write(
722            &source_file,
723            r#"
724            use serde::{Deserialize, Serialize};
725
726            #[derive(Debug, Clone, Serialize, Deserialize)]
727            pub struct Payload {
728                pub value: String,
729                pub count: i32,
730            }
731
732            #[tauri::command]
733            pub fn fetch_payload() -> Result<Payload, String> {
734                Ok(Payload {
735                    value: "one".to_string(),
736                    count: 2,
737                })
738            }
739        "#,
740        )
741        .unwrap();
742
743        run_generation(&build_system, &config);
744        let types_after = read_generated(&output_path, "types.ts");
745
746        assert_ne!(types_before, types_after);
747        assert!(types_after.contains("count: number"));
748    }
749
750    #[test]
751    fn test_generate_bindings_regenerates_when_events_change() {
752        let temp_dir = TempDir::new().unwrap();
753        let project_path = temp_dir.path().join("src-tauri");
754        let output_path = temp_dir.path().join("generated");
755        std::fs::create_dir_all(&project_path).unwrap();
756
757        let source_file = project_path.join("main.rs");
758        std::fs::write(
759            &source_file,
760            r#"
761            use serde::{Deserialize, Serialize};
762            use tauri::Manager;
763
764            #[derive(Debug, Clone, Serialize, Deserialize)]
765            pub struct Payload {
766                pub value: String,
767            }
768
769            #[tauri::command]
770            pub fn emit_event(app: tauri::AppHandle) -> Result<(), String> {
771                app.emit("first-event", Payload {
772                    value: "one".to_string(),
773                }).ok();
774                Ok(())
775            }
776        "#,
777        )
778        .unwrap();
779
780        let config = create_build_config(&project_path, &output_path);
781
782        let build_system = BuildSystem::new(false, false);
783        run_generation(&build_system, &config);
784        let events_before = read_generated(&output_path, "events.ts");
785
786        std::fs::write(
787            &source_file,
788            r#"
789            use serde::{Deserialize, Serialize};
790            use tauri::Manager;
791
792            #[derive(Debug, Clone, Serialize, Deserialize)]
793            pub struct Payload {
794                pub value: String,
795            }
796
797            #[tauri::command]
798            pub fn emit_event(app: tauri::AppHandle) -> Result<(), String> {
799                app.emit("second-event", Payload {
800                    value: "two".to_string(),
801                }).ok();
802                Ok(())
803            }
804        "#,
805        )
806        .unwrap();
807
808        run_generation(&build_system, &config);
809
810        let events_after = read_generated(&output_path, "events.ts");
811        assert_ne!(events_before, events_after);
812        assert!(events_after.contains("second-event"));
813        assert!(!events_after.contains("first-event"));
814    }
815}