|
@ -1,11 +1,11 @@ |
|
|
#!/usr/bin/env node |
|
|
#!/usr/bin/env tsx |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* TimeSafari Asset Configuration Validator |
|
|
* TimeSafari Asset Configuration Validator |
|
|
* Validates capacitor-assets configuration against schema and source files |
|
|
* Validates capacitor-assets configuration against schema and source files |
|
|
* Author: Matthew Raymer |
|
|
* Author: Matthew Raymer |
|
|
* |
|
|
* |
|
|
* Usage: node scripts/assets-validator.js [config-path] |
|
|
* Usage: tsx scripts/assets-validator.ts [config-path] |
|
|
*/ |
|
|
*/ |
|
|
|
|
|
|
|
|
import fs from 'fs'; |
|
|
import fs from 'fs'; |
|
@ -16,50 +16,71 @@ const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = path.dirname(__filename); |
|
|
const __dirname = path.dirname(__filename); |
|
|
const PROJECT_ROOT = path.dirname(__dirname); |
|
|
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 |
|
|
* Load and parse JSON file |
|
|
* @param {string} filePath - Path to JSON file |
|
|
* @param filePath - Path to JSON file |
|
|
* @returns {Object} Parsed JSON object |
|
|
* @returns Parsed JSON object |
|
|
*/ |
|
|
*/ |
|
|
function loadJsonFile(filePath) { |
|
|
function loadJsonFile(filePath: string): AssetConfig { |
|
|
try { |
|
|
try { |
|
|
const content = fs.readFileSync(filePath, 'utf8'); |
|
|
const content = fs.readFileSync(filePath, 'utf8'); |
|
|
return JSON.parse(content); |
|
|
return JSON.parse(content); |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
throw new Error(`Failed to load ${filePath}: ${error.message}`); |
|
|
throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Validate configuration against schema |
|
|
* Validate configuration against schema |
|
|
* @param {Object} config - Configuration object to validate |
|
|
* @param config - Configuration object to validate |
|
|
* @param {Object} schema - JSON schema for validation |
|
|
* @returns Array of validation errors |
|
|
* @returns {Array} Array of validation errors |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
function validateAgainstSchema(config, schema) { |
|
|
function validateAgainstSchema(config: AssetConfig): ValidationError[] { |
|
|
const errors = []; |
|
|
const errors: ValidationError[] = []; |
|
|
|
|
|
|
|
|
// Basic structure validation
|
|
|
// Basic structure validation
|
|
|
if (!config.icon || !config.splash) { |
|
|
if (!config.icon || !config.splash) { |
|
|
errors.push('Configuration must contain both "icon" and "splash" sections'); |
|
|
errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Icon validation
|
|
|
// Icon validation
|
|
|
if (config.icon) { |
|
|
if (config.icon) { |
|
|
if (!config.icon.source) { |
|
|
if (!config.icon.source) { |
|
|
errors.push('Icon section must contain "source" field'); |
|
|
errors.push({ message: 'Icon section must contain "source" field' }); |
|
|
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { |
|
|
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) { |
|
|
errors.push('Icon source must be a PNG or SVG file in resources/ directory'); |
|
|
errors.push({ message: 'Icon source must be a PNG or SVG file in resources/ directory' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Android adaptive icon validation
|
|
|
// Android adaptive icon validation
|
|
|
if (config.icon.android?.adaptive) { |
|
|
if (config.icon.android?.adaptive) { |
|
|
const adaptive = config.icon.android.adaptive; |
|
|
const adaptive = config.icon.android.adaptive; |
|
|
if (!adaptive.foreground || !adaptive.background) { |
|
|
if (!adaptive.foreground || !adaptive.background) { |
|
|
errors.push('Android adaptive icon must have both foreground and background'); |
|
|
errors.push({ message: 'Android adaptive icon must have both foreground and background' }); |
|
|
} |
|
|
} |
|
|
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { |
|
|
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) { |
|
|
errors.push('Android adaptive foreground must be a PNG or SVG file in resources/ directory'); |
|
|
errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' }); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -67,13 +88,13 @@ function validateAgainstSchema(config, schema) { |
|
|
// Splash validation
|
|
|
// Splash validation
|
|
|
if (config.splash) { |
|
|
if (config.splash) { |
|
|
if (!config.splash.source) { |
|
|
if (!config.splash.source) { |
|
|
errors.push('Splash section must contain "source" field'); |
|
|
errors.push({ message: 'Splash section must contain "source" field' }); |
|
|
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { |
|
|
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) { |
|
|
errors.push('Splash source must be a PNG or SVG file in resources/ directory'); |
|
|
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)) { |
|
|
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'); |
|
|
errors.push({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' }); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -82,12 +103,12 @@ function validateAgainstSchema(config, schema) { |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Validate that source files exist |
|
|
* Validate that source files exist |
|
|
* @param {Object} config - Configuration object |
|
|
* @param config - Configuration object |
|
|
* @returns {Array} Array of missing file errors |
|
|
* @returns Array of missing file errors |
|
|
*/ |
|
|
*/ |
|
|
function validateSourceFiles(config) { |
|
|
function validateSourceFiles(config: AssetConfig): ValidationError[] { |
|
|
const errors = []; |
|
|
const errors: ValidationError[] = []; |
|
|
const requiredFiles = new Set(); |
|
|
const requiredFiles = new Set<string>(); |
|
|
|
|
|
|
|
|
// Collect all required source files
|
|
|
// Collect all required source files
|
|
|
if (config.icon?.source) requiredFiles.add(config.icon.source); |
|
|
if (config.icon?.source) requiredFiles.add(config.icon.source); |
|
@ -100,7 +121,7 @@ function validateSourceFiles(config) { |
|
|
requiredFiles.forEach(file => { |
|
|
requiredFiles.forEach(file => { |
|
|
const filePath = path.join(PROJECT_ROOT, file); |
|
|
const filePath = path.join(PROJECT_ROOT, file); |
|
|
if (!fs.existsSync(filePath)) { |
|
|
if (!fs.existsSync(filePath)) { |
|
|
errors.push(`Source file not found: ${file}`); |
|
|
errors.push({ message: `Source file not found: ${file}` }); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
@ -109,12 +130,12 @@ function validateSourceFiles(config) { |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Validate target directories are writable |
|
|
* Validate target directories are writable |
|
|
* @param {Object} config - Configuration object |
|
|
* @param config - Configuration object |
|
|
* @returns {Array} Array of directory validation errors |
|
|
* @returns Array of directory validation errors |
|
|
*/ |
|
|
*/ |
|
|
function validateTargetDirectories(config) { |
|
|
function validateTargetDirectories(config: AssetConfig): ValidationError[] { |
|
|
const errors = []; |
|
|
const errors: ValidationError[] = []; |
|
|
const targetDirs = new Set(); |
|
|
const targetDirs = new Set<string>(); |
|
|
|
|
|
|
|
|
// Collect all target directories
|
|
|
// Collect all target directories
|
|
|
if (config.icon?.android?.target) targetDirs.add(config.icon.android.target); |
|
|
if (config.icon?.android?.target) targetDirs.add(config.icon.android.target); |
|
@ -129,9 +150,9 @@ function validateTargetDirectories(config) { |
|
|
const parentDir = path.dirname(dirPath); |
|
|
const parentDir = path.dirname(dirPath); |
|
|
|
|
|
|
|
|
if (!fs.existsSync(parentDir)) { |
|
|
if (!fs.existsSync(parentDir)) { |
|
|
errors.push(`Parent directory does not exist: ${parentDir}`); |
|
|
errors.push({ message: `Parent directory does not exist: ${parentDir}` }); |
|
|
} else if (!fs.statSync(parentDir).isDirectory()) { |
|
|
} else if (!fs.statSync(parentDir).isDirectory()) { |
|
|
errors.push(`Parent path is not a directory: ${parentDir}`); |
|
|
errors.push({ message: `Parent path is not a directory: ${parentDir}` }); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
@ -140,10 +161,10 @@ function validateTargetDirectories(config) { |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Main validation function |
|
|
* Main validation function |
|
|
* @param {string} configPath - Path to configuration file |
|
|
* @param configPath - Path to configuration file |
|
|
* @returns {boolean} True if validation passes |
|
|
* @returns True if validation passes |
|
|
*/ |
|
|
*/ |
|
|
function validateConfiguration(configPath) { |
|
|
function validateConfiguration(configPath: string): boolean { |
|
|
console.log('🔍 Validating TimeSafari asset configuration...'); |
|
|
console.log('🔍 Validating TimeSafari asset configuration...'); |
|
|
console.log(`📁 Config file: ${configPath}`); |
|
|
console.log(`📁 Config file: ${configPath}`); |
|
|
console.log(`📁 Project root: ${PROJECT_ROOT}`); |
|
|
console.log(`📁 Project root: ${PROJECT_ROOT}`); |
|
@ -159,7 +180,7 @@ function validateConfiguration(configPath) { |
|
|
console.log('✅ Schema file loaded successfully'); |
|
|
console.log('✅ Schema file loaded successfully'); |
|
|
|
|
|
|
|
|
// Perform validations
|
|
|
// Perform validations
|
|
|
const schemaErrors = validateAgainstSchema(config, schema); |
|
|
const schemaErrors = validateAgainstSchema(config); |
|
|
const fileErrors = validateSourceFiles(config); |
|
|
const fileErrors = validateSourceFiles(config); |
|
|
const dirErrors = validateTargetDirectories(config); |
|
|
const dirErrors = validateTargetDirectories(config); |
|
|
|
|
|
|
|
@ -179,13 +200,13 @@ function validateConfiguration(configPath) { |
|
|
} else { |
|
|
} else { |
|
|
console.error('❌ Validation failed with the following errors:'); |
|
|
console.error('❌ Validation failed with the following errors:'); |
|
|
allErrors.forEach((error, index) => { |
|
|
allErrors.forEach((error, index) => { |
|
|
console.error(` ${index + 1}. ${error}`); |
|
|
console.error(` ${index + 1}. ${error.message}`); |
|
|
}); |
|
|
}); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
console.error('❌ Validation failed:', error.message); |
|
|
console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error)); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
@ -193,7 +214,7 @@ function validateConfiguration(configPath) { |
|
|
/** |
|
|
/** |
|
|
* Main execution function |
|
|
* Main execution function |
|
|
*/ |
|
|
*/ |
|
|
function main() { |
|
|
function main(): void { |
|
|
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); |
|
|
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json'); |
|
|
|
|
|
|
|
|
if (!fs.existsSync(configPath)) { |
|
|
if (!fs.existsSync(configPath)) { |
|
@ -201,7 +222,7 @@ function main() { |
|
|
console.log(''); |
|
|
console.log(''); |
|
|
console.log('💡 Available options:'); |
|
|
console.log('💡 Available options:'); |
|
|
console.log(' - Use default: capacitor-assets.config.json'); |
|
|
console.log(' - Use default: capacitor-assets.config.json'); |
|
|
console.log(' - Specify path: node scripts/assets-validator.js path/to/config.json'); |
|
|
console.log(' - Specify path: tsx scripts/assets-validator.ts path/to/config.json'); |
|
|
console.log(' - Generate config: npm run assets:config'); |
|
|
console.log(' - Generate config: npm run assets:config'); |
|
|
process.exit(1); |
|
|
process.exit(1); |
|
|
} |
|
|
} |
|
@ -215,4 +236,4 @@ if (import.meta.url === `file://${process.argv[1]}`) { |
|
|
main(); |
|
|
main(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories }; |
|
|
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig }; |