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