tauri_typegen/build/
output_manager.rs

1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6#[derive(Error, Debug)]
7pub enum OutputError {
8    #[error("IO error: {0}")]
9    Io(#[from] std::io::Error),
10    #[error("Invalid output path: {0}")]
11    InvalidPath(String),
12    #[error("Permission denied: {0}")]
13    PermissionDenied(String),
14}
15
16pub struct OutputManager {
17    output_dir: PathBuf,
18    managed_files: HashSet<String>,
19    backup_dir: Option<PathBuf>,
20}
21
22impl OutputManager {
23    pub fn new<P: AsRef<Path>>(output_dir: P) -> Self {
24        Self {
25            output_dir: output_dir.as_ref().to_path_buf(),
26            managed_files: HashSet::new(),
27            backup_dir: None,
28        }
29    }
30
31    pub fn with_backup<P: AsRef<Path>>(output_dir: P, backup_dir: Option<P>) -> Self {
32        Self {
33            output_dir: output_dir.as_ref().to_path_buf(),
34            managed_files: HashSet::new(),
35            backup_dir: backup_dir.map(|p| p.as_ref().to_path_buf()),
36        }
37    }
38
39    /// Ensure the output directory exists and is writable
40    pub fn prepare_output_directory(&self) -> Result<(), OutputError> {
41        if !self.output_dir.exists() {
42            fs::create_dir_all(&self.output_dir).map_err(|e| {
43                OutputError::PermissionDenied(format!(
44                    "Cannot create output directory {}: {}",
45                    self.output_dir.display(),
46                    e
47                ))
48            })?;
49        }
50
51        // Test write permissions by creating a temporary file
52        let test_file = self.output_dir.join(".write_test");
53        fs::write(&test_file, "test").map_err(|e| {
54            OutputError::PermissionDenied(format!(
55                "Cannot write to output directory {}: {}",
56                self.output_dir.display(),
57                e
58            ))
59        })?;
60        fs::remove_file(&test_file).ok(); // Ignore errors on cleanup
61
62        Ok(())
63    }
64
65    /// Register a file as managed by this generator
66    pub fn register_managed_file(&mut self, filename: &str) {
67        self.managed_files.insert(filename.to_string());
68    }
69
70    /// Clean up old generated files that are no longer needed
71    pub fn cleanup_old_files(&self, current_files: &[String]) -> Result<Vec<String>, OutputError> {
72        let mut cleaned_files = Vec::new();
73
74        if !self.output_dir.exists() {
75            return Ok(cleaned_files);
76        }
77
78        let current_set: HashSet<String> = current_files.iter().cloned().collect();
79
80        // Read the directory and find files that look like they were generated
81        let entries = fs::read_dir(&self.output_dir)?;
82
83        for entry in entries {
84            let entry = entry?;
85            let path = entry.path();
86
87            if path.is_file() {
88                if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
89                    // Only clean up files that look like generated TypeScript files
90                    if self.is_generated_file(filename) && !current_set.contains(filename) {
91                        self.backup_and_remove_file(&path)?;
92                        cleaned_files.push(filename.to_string());
93                    }
94                }
95            }
96        }
97
98        Ok(cleaned_files)
99    }
100
101    /// Check if a file appears to be a generated file based on naming patterns
102    fn is_generated_file(&self, filename: &str) -> bool {
103        // Check for common generated file patterns
104        let generated_patterns = [
105            "types.ts",
106            "types.d.ts",
107            "commands.ts",
108            "commands.d.ts",
109            "schemas.ts",
110            "schemas.d.ts",
111            "index.ts",
112            "index.d.ts",
113            "models.ts",
114            "models.d.ts",
115            "bindings.ts",
116            "bindings.d.ts",
117        ];
118
119        generated_patterns.contains(&filename)
120            || filename.starts_with("generated_")
121            || filename.contains("_generated")
122            || self.managed_files.contains(filename)
123    }
124
125    /// Backup a file before removing it
126    fn backup_and_remove_file(&self, file_path: &Path) -> Result<(), OutputError> {
127        if let Some(backup_dir) = &self.backup_dir {
128            if !backup_dir.exists() {
129                fs::create_dir_all(backup_dir)?;
130            }
131
132            if let Some(filename) = file_path.file_name() {
133                let backup_path = backup_dir.join(format!(
134                    "{}.backup.{}",
135                    filename.to_string_lossy(),
136                    chrono::Utc::now().timestamp()
137                ));
138                fs::copy(file_path, backup_path)?;
139            }
140        }
141
142        fs::remove_file(file_path)?;
143        Ok(())
144    }
145
146    /// Write a file to the output directory with proper error handling
147    pub fn write_file(&self, filename: &str, content: &str) -> Result<PathBuf, OutputError> {
148        let file_path = self.output_dir.join(filename);
149
150        // Ensure parent directory exists
151        if let Some(parent) = file_path.parent() {
152            if !parent.exists() {
153                fs::create_dir_all(parent)?;
154            }
155        }
156
157        // Write with atomic operation (write to temp file first, then rename)
158        let temp_path = file_path.with_extension("tmp");
159        fs::write(&temp_path, content)?;
160        fs::rename(&temp_path, &file_path)?;
161
162        Ok(file_path)
163    }
164
165    /// Verify that all expected files were generated
166    pub fn verify_output(&self, expected_files: &[String]) -> Result<Vec<String>, OutputError> {
167        let mut missing_files = Vec::new();
168
169        for expected in expected_files {
170            let file_path = self.output_dir.join(expected);
171            if !file_path.exists() {
172                missing_files.push(expected.clone());
173            }
174        }
175
176        Ok(missing_files)
177    }
178
179    /// Get metadata about generated files
180    pub fn get_generation_metadata(&self) -> Result<GenerationMetadata, OutputError> {
181        let mut metadata = GenerationMetadata {
182            output_directory: self.output_dir.clone(),
183            generated_at: chrono::Utc::now(),
184            files: Vec::new(),
185            total_size: 0,
186        };
187
188        if !self.output_dir.exists() {
189            return Ok(metadata);
190        }
191
192        let entries = fs::read_dir(&self.output_dir)?;
193
194        for entry in entries {
195            let entry = entry?;
196            let path = entry.path();
197
198            if path.is_file() {
199                if let Ok(metadata_entry) = entry.metadata() {
200                    let size = metadata_entry.len();
201                    metadata.total_size += size;
202
203                    if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
204                        metadata.files.push(FileMetadata {
205                            name: filename.to_string(),
206                            path: path.clone(),
207                            size,
208                            modified: metadata_entry.modified().ok(),
209                        });
210                    }
211                }
212            }
213        }
214
215        Ok(metadata)
216    }
217
218    /// Finalize the generation process
219    pub fn finalize_generation(&mut self, generated_files: &[String]) -> Result<(), OutputError> {
220        self.prepare_output_directory()?;
221
222        // Register all generated files as managed
223        for file in generated_files {
224            self.register_managed_file(file);
225        }
226
227        // Clean up old files
228        let cleaned = self.cleanup_old_files(generated_files)?;
229        if !cleaned.is_empty() {
230            eprintln!("Cleaned up {} old generated files", cleaned.len());
231        }
232
233        // Verify all expected files exist
234        let missing = self.verify_output(generated_files)?;
235        if !missing.is_empty() {
236            return Err(OutputError::InvalidPath(format!(
237                "Missing generated files: {}",
238                missing.join(", ")
239            )));
240        }
241
242        Ok(())
243    }
244
245    /// Create a summary report of the generation process
246    pub fn create_summary_report(&self) -> Result<String, OutputError> {
247        let metadata = self.get_generation_metadata()?;
248
249        let mut report = String::new();
250        report.push_str("# TypeScript Generation Summary\n\n");
251        report.push_str(&format!(
252            "Generated at: {}\n",
253            metadata.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
254        ));
255        report.push_str(&format!(
256            "Output directory: {}\n",
257            metadata.output_directory.display()
258        ));
259        report.push_str(&format!("Total files: {}\n", metadata.files.len()));
260        report.push_str(&format!("Total size: {} bytes\n\n", metadata.total_size));
261
262        report.push_str("## Generated Files\n\n");
263        for file in &metadata.files {
264            report.push_str(&format!("- **{}** ({} bytes)\n", file.name, file.size));
265        }
266
267        Ok(report)
268    }
269}
270
271#[derive(Debug)]
272pub struct GenerationMetadata {
273    pub output_directory: PathBuf,
274    pub generated_at: chrono::DateTime<chrono::Utc>,
275    pub files: Vec<FileMetadata>,
276    pub total_size: u64,
277}
278
279#[derive(Debug)]
280pub struct FileMetadata {
281    pub name: String,
282    pub path: PathBuf,
283    pub size: u64,
284    pub modified: Option<std::time::SystemTime>,
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use tempfile::TempDir;
291
292    #[test]
293    fn test_prepare_output_directory() {
294        let temp_dir = TempDir::new().unwrap();
295        let output_path = temp_dir.path().join("output");
296
297        let manager = OutputManager::new(&output_path);
298        manager.prepare_output_directory().unwrap();
299
300        assert!(output_path.exists());
301        assert!(output_path.is_dir());
302    }
303
304    #[test]
305    fn test_write_file() {
306        let temp_dir = TempDir::new().unwrap();
307        let manager = OutputManager::new(temp_dir.path());
308
309        manager.prepare_output_directory().unwrap();
310
311        let content = "export interface Test { name: string; }";
312        let file_path = manager.write_file("test.ts", content).unwrap();
313
314        assert!(file_path.exists());
315        let written_content = fs::read_to_string(&file_path).unwrap();
316        assert_eq!(written_content, content);
317    }
318
319    #[test]
320    fn test_is_generated_file() {
321        let temp_dir = TempDir::new().unwrap();
322        let manager = OutputManager::new(temp_dir.path());
323
324        // Test standard patterns
325        assert!(manager.is_generated_file("types.ts"));
326        assert!(manager.is_generated_file("commands.ts"));
327        assert!(manager.is_generated_file("schemas.ts"));
328        assert!(manager.is_generated_file("index.ts"));
329
330        // Test prefix/suffix patterns
331        assert!(manager.is_generated_file("generated_models.ts"));
332        assert!(manager.is_generated_file("api_generated.ts"));
333
334        // Test non-generated files
335        assert!(!manager.is_generated_file("user_code.ts"));
336        assert!(!manager.is_generated_file("main.ts"));
337        assert!(!manager.is_generated_file("app.vue"));
338    }
339
340    #[test]
341    fn test_cleanup_old_files() {
342        let temp_dir = TempDir::new().unwrap();
343        let manager = OutputManager::new(temp_dir.path());
344
345        manager.prepare_output_directory().unwrap();
346
347        // Create some files
348        fs::write(temp_dir.path().join("types.ts"), "old content").unwrap();
349        fs::write(temp_dir.path().join("commands.ts"), "old content").unwrap();
350        fs::write(temp_dir.path().join("user_file.ts"), "user content").unwrap();
351
352        // Current generation only includes types.ts
353        let current_files = vec!["types.ts".to_string()];
354
355        let cleaned = manager.cleanup_old_files(&current_files).unwrap();
356
357        // Should clean up commands.ts but not user_file.ts or types.ts
358        assert_eq!(cleaned.len(), 1);
359        assert!(cleaned.contains(&"commands.ts".to_string()));
360
361        assert!(temp_dir.path().join("types.ts").exists());
362        assert!(!temp_dir.path().join("commands.ts").exists());
363        assert!(temp_dir.path().join("user_file.ts").exists());
364    }
365
366    #[test]
367    fn test_verify_output() {
368        let temp_dir = TempDir::new().unwrap();
369        let manager = OutputManager::new(temp_dir.path());
370
371        manager.prepare_output_directory().unwrap();
372
373        // Create one expected file
374        fs::write(temp_dir.path().join("types.ts"), "content").unwrap();
375
376        let expected = vec!["types.ts".to_string(), "commands.ts".to_string()];
377        let missing = manager.verify_output(&expected).unwrap();
378
379        assert_eq!(missing.len(), 1);
380        assert!(missing.contains(&"commands.ts".to_string()));
381    }
382
383    #[test]
384    fn test_generation_metadata() {
385        let temp_dir = TempDir::new().unwrap();
386        let manager = OutputManager::new(temp_dir.path());
387
388        manager.prepare_output_directory().unwrap();
389
390        // Create some test files
391        fs::write(temp_dir.path().join("types.ts"), "interface Test {}").unwrap();
392        fs::write(
393            temp_dir.path().join("commands.ts"),
394            "export function test() {}",
395        )
396        .unwrap();
397
398        let metadata = manager.get_generation_metadata().unwrap();
399
400        assert_eq!(metadata.files.len(), 2);
401        assert!(metadata.total_size > 0);
402        assert_eq!(metadata.output_directory, temp_dir.path());
403
404        // Check individual files
405        let types_file = metadata
406            .files
407            .iter()
408            .find(|f| f.name == "types.ts")
409            .unwrap();
410        assert_eq!(types_file.size, 17); // Length of "interface Test {}"
411    }
412}