#!/usr/bin/env node /** * TimeSafari Asset Configuration Validator * Validates capacitor-assets configuration against schema and source files * Author: Matthew Raymer * * Usage: node scripts/assets-validator.js [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); /** * Load and parse JSON file * @param {string} filePath - Path to JSON file * @returns {Object} Parsed JSON object */ function loadJsonFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); return JSON.parse(content); } catch (error) { throw new Error(`Failed to load ${filePath}: ${error.message}`); } } /** * Validate configuration against schema * @param {Object} config - Configuration object to validate * @param {Object} schema - JSON schema for validation * @returns {Array} Array of validation errors */ function validateAgainstSchema(config, schema) { const errors = []; // Basic structure validation if (!config.icon || !config.splash) { errors.push('Configuration must contain both "icon" and "splash" sections'); } // Icon validation if (config.icon) { if (!config.icon.source) { errors.push('Icon section must contain "source" field'); } else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { errors.push('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('Android adaptive icon must have both foreground and background'); } if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { errors.push('Android adaptive foreground must be a PNG or SVG file in resources/ directory'); } } } // Splash validation if (config.splash) { if (!config.splash.source) { errors.push('Splash section must contain "source" field'); } else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { errors.push('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('Dark splash source must be a PNG or SVG file in resources/ directory'); } } return errors; } /** * Validate that source files exist * @param {Object} config - Configuration object * @returns {Array} Array of missing file errors */ function validateSourceFiles(config) { const errors = []; const requiredFiles = new Set(); // 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(`Source file not found: ${file}`); } }); return errors; } /** * Validate target directories are writable * @param {Object} config - Configuration object * @returns {Array} Array of directory validation errors */ function validateTargetDirectories(config) { const errors = []; const targetDirs = new Set(); // 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(`Parent directory does not exist: ${parentDir}`); } else if (!fs.statSync(parentDir).isDirectory()) { errors.push(`Parent path is not a directory: ${parentDir}`); } }); return errors; } /** * Main validation function * @param {string} configPath - Path to configuration file * @returns {boolean} True if validation passes */ function validateConfiguration(configPath) { 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, schema); 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}`); }); return false; } } catch (error) { console.error('❌ Validation failed:', error.message); return false; } } /** * Main execution function */ function main() { 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: node scripts/assets-validator.js 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 };