You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							239 lines
						
					
					
						
							7.9 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							239 lines
						
					
					
						
							7.9 KiB
						
					
					
				| #!/usr/bin/env tsx | |
| 
 | |
| /** | |
|  * TimeSafari Asset Configuration Validator | |
|  * Validates capacitor-assets configuration against schema and source files | |
|  * Author: Matthew Raymer | |
|  *  | |
|  * Usage: tsx scripts/assets-validator.ts [config-path] | |
|  */ | |
| 
 | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| 
 | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const PROJECT_ROOT = path.dirname(__dirname); | |
| 
 | |
| // TypeScript interfaces for validation | |
| interface ValidationError { | |
|   message: string; | |
| } | |
| 
 | |
| interface AssetConfig { | |
|   icon?: { | |
|     source?: string; | |
|     android?: { | |
|       adaptive?: { | |
|         foreground?: string; | |
|         background?: string; | |
|         monochrome?: string; | |
|       }; | |
|     }; | |
|   }; | |
|   splash?: { | |
|     source?: string; | |
|     darkSource?: string; | |
|   }; | |
| } | |
| 
 | |
| /** | |
|  * Load and parse JSON file | |
|  * @param filePath - Path to JSON file | |
|  * @returns Parsed JSON object | |
|  */ | |
| function loadJsonFile(filePath: string): AssetConfig { | |
|   try { | |
|     const content = fs.readFileSync(filePath, 'utf8'); | |
|     return JSON.parse(content); | |
|   } catch (error) { | |
|     throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`); | |
|   } | |
| } | |
| 
 | |
| /** | |
|  * Validate configuration against schema | |
|  * @param config - Configuration object to validate | |
|  * @returns Array of validation errors | |
|  */ | |
| function validateAgainstSchema(config: AssetConfig): ValidationError[] { | |
|   const errors: ValidationError[] = []; | |
| 
 | |
|   // Basic structure validation | |
|   if (!config.icon || !config.splash) { | |
|     errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' }); | |
|   } | |
| 
 | |
|   // Icon validation | |
|   if (config.icon) { | |
|     if (!config.icon.source) { | |
|       errors.push({ message: 'Icon section must contain "source" field' }); | |
|     } else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { | |
|       errors.push({ message: 'Icon source must be a PNG or SVG file in resources/ directory' }); | |
|     } | |
| 
 | |
|     // Android adaptive icon validation | |
|     if (config.icon.android?.adaptive) { | |
|       const adaptive = config.icon.android.adaptive; | |
|       if (!adaptive.foreground || !adaptive.background) { | |
|         errors.push({ message: 'Android adaptive icon must have both foreground and background' }); | |
|       } | |
|       if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { | |
|         errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' }); | |
|       } | |
|     } | |
|   } | |
| 
 | |
|   // Splash validation | |
|   if (config.splash) { | |
|     if (!config.splash.source) { | |
|       errors.push({ message: 'Splash section must contain "source" field' }); | |
|     } else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { | |
|       errors.push({ message: 'Splash source must be a PNG or SVG file in resources/ directory' }); | |
|     } | |
| 
 | |
|     if (config.splash.darkSource && !/^resources\/.*\.(png|svg)$/.test(config.splash.darkSource)) { | |
|       errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' }); | |
|     } | |
|   } | |
| 
 | |
|   return errors; | |
| } | |
| 
 | |
| /** | |
|  * Validate that source files exist | |
|  * @param config - Configuration object | |
|  * @returns Array of missing file errors | |
|  */ | |
| function validateSourceFiles(config: AssetConfig): ValidationError[] { | |
|   const errors: ValidationError[] = []; | |
|   const requiredFiles = new Set<string>(); | |
| 
 | |
|   // Collect all required source files | |
|   if (config.icon?.source) requiredFiles.add(config.icon.source); | |
|   if (config.icon?.android?.adaptive?.foreground) requiredFiles.add(config.icon.android.adaptive.foreground); | |
|   if (config.icon?.android?.adaptive?.monochrome) requiredFiles.add(config.icon.android.adaptive.monochrome); | |
|   if (config.splash?.source) requiredFiles.add(config.splash.source); | |
|   if (config.splash?.darkSource) requiredFiles.add(config.splash.darkSource); | |
| 
 | |
|   // Check each file exists | |
|   requiredFiles.forEach(file => { | |
|     const filePath = path.join(PROJECT_ROOT, file); | |
|     if (!fs.existsSync(filePath)) { | |
|       errors.push({ message: `Source file not found: ${file}` }); | |
|     } | |
|   }); | |
| 
 | |
|   return errors; | |
| } | |
| 
 | |
| /** | |
|  * Validate target directories are writable | |
|  * @param config - Configuration object | |
|  * @returns Array of directory validation errors | |
|  */ | |
| function validateTargetDirectories(config: AssetConfig): ValidationError[] { | |
|   const errors: ValidationError[] = []; | |
|   const targetDirs = new Set<string>(); | |
| 
 | |
|   // Collect all target directories | |
|   if (config.icon?.android?.target) targetDirs.add(config.icon.android.target); | |
|   if (config.icon?.ios?.target) targetDirs.add(config.icon.ios.target); | |
|   if (config.icon?.web?.target) targetDirs.add(config.icon.web.target); | |
|   if (config.splash?.android?.target) targetDirs.add(config.splash.android.target); | |
|   if (config.splash?.ios?.target) targetDirs.add(config.splash.ios.target); | |
| 
 | |
|   // Check each target directory | |
|   targetDirs.forEach(dir => { | |
|     const dirPath = path.join(PROJECT_ROOT, dir); | |
|     const parentDir = path.dirname(dirPath); | |
|      | |
|     if (!fs.existsSync(parentDir)) { | |
|       errors.push({ message: `Parent directory does not exist: ${parentDir}` }); | |
|     } else if (!fs.statSync(parentDir).isDirectory()) { | |
|       errors.push({ message: `Parent path is not a directory: ${parentDir}` }); | |
|     } | |
|   }); | |
| 
 | |
|   return errors; | |
| } | |
| 
 | |
| /** | |
|  * Main validation function | |
|  * @param configPath - Path to configuration file | |
|  * @returns True if validation passes | |
|  */ | |
| function validateConfiguration(configPath: string): boolean { | |
|   console.log('🔍 Validating TimeSafari asset configuration...'); | |
|   console.log(`📁 Config file: ${configPath}`); | |
|   console.log(`📁 Project root: ${PROJECT_ROOT}`); | |
| 
 | |
|   try { | |
|     // Load configuration | |
|     const config = loadJsonFile(configPath); | |
|     console.log('✅ Configuration file loaded successfully'); | |
| 
 | |
|     // Load schema | |
|     const schemaPath = path.join(PROJECT_ROOT, 'config', 'assets', 'schema.json'); | |
|     const schema = loadJsonFile(schemaPath); | |
|     console.log('✅ Schema file loaded successfully'); | |
| 
 | |
|     // Perform validations | |
|     const schemaErrors = validateAgainstSchema(config); | |
|     const fileErrors = validateSourceFiles(config); | |
|     const dirErrors = validateTargetDirectories(config); | |
| 
 | |
|     // Report results | |
|     const allErrors = [...schemaErrors, ...fileErrors, ...dirErrors]; | |
| 
 | |
|     if (allErrors.length === 0) { | |
|       console.log('🎉 All validations passed successfully!'); | |
|       console.log(''); | |
|       console.log('📋 Configuration summary:'); | |
|       console.log(`   Icon source: ${config.icon?.source || 'NOT SET'}`); | |
|       console.log(`   Splash source: ${config.splash?.source || 'NOT SET'}`); | |
|       console.log(`   Dark splash: ${config.splash?.darkSource || 'NOT SET'}`); | |
|       console.log(`   Android adaptive: ${config.icon?.android?.adaptive ? 'ENABLED' : 'DISABLED'}`); | |
|       console.log(`   iOS LaunchScreen: ${config.splash?.ios?.useStoryBoard ? 'ENABLED' : 'DISABLED'}`); | |
|       return true; | |
|     } else { | |
|       console.error('❌ Validation failed with the following errors:'); | |
|       allErrors.forEach((error, index) => { | |
|         console.error(`   ${index + 1}. ${error.message}`); | |
|       }); | |
|       return false; | |
|     } | |
| 
 | |
|   } catch (error) { | |
|     console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error)); | |
|     return false; | |
|   } | |
| } | |
| 
 | |
| /** | |
|  * Main execution function | |
|  */ | |
| function main(): void { | |
|   const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); | |
|    | |
|   if (!fs.existsSync(configPath)) { | |
|     console.error(`❌ Configuration file not found: ${configPath}`); | |
|     console.log(''); | |
|     console.log('💡 Available options:'); | |
|     console.log('   - Use default: capacitor-assets.config.json'); | |
|     console.log('   - Specify path: tsx scripts/assets-validator.ts path/to/config.json'); | |
|     console.log('   - Generate config: npm run assets:config'); | |
|     process.exit(1); | |
|   } | |
| 
 | |
|   const success = validateConfiguration(configPath); | |
|   process.exit(success ? 0 : 1); | |
| } | |
| 
 | |
| // Run if called directly | |
| if (import.meta.url === `file://${process.argv[1]}`) { | |
|   main(); | |
| } | |
| 
 | |
| export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig };
 | |
| 
 |