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 };
							 | 
						|
								
							 |