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

#!/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 };