forked from trent_larson/crowd-funder-for-time-pwa
feat(assets): standardize asset configuration with capacitor-assets
- Replace manual ImageMagick scripts with official capacitor-assets toolchain - Consolidate duplicate asset sources to single resources/ directory - Implement comprehensive asset configuration schema and validation - Add CI safeguards for asset validation and platform asset detection - Convert capacitor.config.json to TypeScript format - Pin Node.js version for deterministic builds - Remove legacy manual asset generation scripts: * generate-icons.sh, generate-ios-assets.sh, generate-android-icons.sh * check-android-resources.sh, check-ios-resources.sh * purge-generated-assets.sh - Add new asset management commands: * assets:config - generate/update configurations * assets:validate - validate configurations * assets:clean - clean generated assets (dev only) * build:native - build with asset generation - Create GitHub Actions workflow for asset validation - Update documentation with new asset management workflow This standardization eliminates asset duplication, improves build reliability, and provides a maintainable asset management system using Capacitor defaults. Breaking Changes: Manual asset generation scripts removed Migration: Assets now sourced from resources/ directory only CI: Automated validation prevents committed platform assets
This commit is contained in:
218
scripts/assets-validator.js
Normal file
218
scripts/assets-validator.js
Normal file
@@ -0,0 +1,218 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user