refactor(assets): convert asset management scripts to TypeScript with tsx
- Replace JavaScript asset scripts with TypeScript equivalents - Install tsx for direct TypeScript execution without compilation - Add proper TypeScript interfaces for AssetConfig and validation - Update package.json scripts to use tsx instead of node - Remove old JavaScript files (assets-config.js, assets-validator.js) - Maintain all existing functionality while improving type safety - Fix module syntax issues that caused build failures on macOS Scripts affected: - assets:config: node → tsx scripts/assets-config.ts - assets:validate: node → tsx scripts/assets-validator.ts Benefits: - Eliminates CommonJS/ES module syntax conflicts - Provides better type safety and IntelliSense - Modernizes development tooling - Ensures cross-platform compatibility
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* TimeSafari Asset Configuration Generator
|
||||
* Generates capacitor-assets configuration files with deterministic outputs
|
||||
* Author: Matthew Raymer
|
||||
*
|
||||
* Usage: node scripts/assets-config.js
|
||||
* Usage: tsx scripts/assets-config.ts
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
@@ -16,12 +16,62 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.dirname(__dirname);
|
||||
|
||||
// TypeScript interfaces for asset configuration
|
||||
interface AdaptiveIconConfig {
|
||||
foreground: string;
|
||||
background: string;
|
||||
monochrome: string;
|
||||
}
|
||||
|
||||
interface AndroidIconConfig {
|
||||
adaptive: AdaptiveIconConfig;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface IOSIconConfig {
|
||||
padding: number;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface WebIconConfig {
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface IconConfig {
|
||||
source: string;
|
||||
android: AndroidIconConfig;
|
||||
ios: IOSIconConfig;
|
||||
web: WebIconConfig;
|
||||
}
|
||||
|
||||
interface AndroidSplashConfig {
|
||||
scale: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface IOSSplashConfig {
|
||||
useStoryBoard: boolean;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface SplashConfig {
|
||||
source: string;
|
||||
darkSource: string;
|
||||
android: AndroidSplashConfig;
|
||||
ios: IOSSplashConfig;
|
||||
}
|
||||
|
||||
interface AssetConfig {
|
||||
icon: IconConfig;
|
||||
splash: SplashConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deterministic capacitor-assets configuration
|
||||
* @returns {Object} Sorted, stable configuration object
|
||||
* @returns Sorted, stable configuration object
|
||||
*/
|
||||
function generateAssetConfig() {
|
||||
const config = {
|
||||
function generateAssetConfig(): AssetConfig {
|
||||
const config: AssetConfig = {
|
||||
icon: {
|
||||
source: "resources/icon.png",
|
||||
android: {
|
||||
@@ -59,10 +109,10 @@ function generateAssetConfig() {
|
||||
|
||||
/**
|
||||
* Sort object keys recursively for deterministic output
|
||||
* @param {Object} obj - Object to sort
|
||||
* @returns {Object} Object with sorted keys
|
||||
* @param obj - Object to sort
|
||||
* @returns Object with sorted keys
|
||||
*/
|
||||
function sortObjectKeys(obj) {
|
||||
function sortObjectKeys(obj: any): any {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
@@ -71,7 +121,7 @@ function sortObjectKeys(obj) {
|
||||
return obj.map(sortObjectKeys);
|
||||
}
|
||||
|
||||
const sorted = {};
|
||||
const sorted: any = {};
|
||||
Object.keys(obj)
|
||||
.sort()
|
||||
.forEach(key => {
|
||||
@@ -84,7 +134,7 @@ function sortObjectKeys(obj) {
|
||||
/**
|
||||
* Validate that required source files exist
|
||||
*/
|
||||
function validateSourceFiles() {
|
||||
function validateSourceFiles(): void {
|
||||
const requiredFiles = [
|
||||
'resources/icon.png',
|
||||
'resources/splash.png',
|
||||
@@ -107,10 +157,10 @@ function validateSourceFiles() {
|
||||
|
||||
/**
|
||||
* Write configuration to file with consistent formatting
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {string} outputPath - Output file path
|
||||
* @param config - Configuration object
|
||||
* @param outputPath - Output file path
|
||||
*/
|
||||
function writeConfig(config, outputPath) {
|
||||
function writeConfig(config: AssetConfig, outputPath: string): void {
|
||||
const jsonString = JSON.stringify(config, null, 2);
|
||||
|
||||
// Ensure consistent line endings and no trailing whitespace
|
||||
@@ -126,7 +176,7 @@ function writeConfig(config, outputPath) {
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
function main() {
|
||||
function main(): void {
|
||||
console.log('🔄 Generating TimeSafari asset configuration...');
|
||||
console.log(`📁 Project root: ${PROJECT_ROOT}`);
|
||||
console.log(`📅 Generated: ${new Date().toISOString()}`);
|
||||
@@ -161,7 +211,7 @@ function main() {
|
||||
console.log(' 4. Use "npm run build:native" for builds');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Configuration generation failed:', error.message);
|
||||
console.error('❌ Configuration generation failed:', error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -171,4 +221,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generateAssetConfig, sortObjectKeys, validateSourceFiles };
|
||||
export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig };
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* TimeSafari Asset Configuration Validator
|
||||
* Validates capacitor-assets configuration against schema and source files
|
||||
* Author: Matthew Raymer
|
||||
*
|
||||
* Usage: node scripts/assets-validator.js [config-path]
|
||||
* Usage: tsx scripts/assets-validator.ts [config-path]
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
@@ -16,50 +16,71 @@ 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 {string} filePath - Path to JSON file
|
||||
* @returns {Object} Parsed JSON object
|
||||
* @param filePath - Path to JSON file
|
||||
* @returns Parsed JSON object
|
||||
*/
|
||||
function loadJsonFile(filePath) {
|
||||
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.message}`);
|
||||
throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration against schema
|
||||
* @param {Object} config - Configuration object to validate
|
||||
* @param {Object} schema - JSON schema for validation
|
||||
* @returns {Array} Array of validation errors
|
||||
* @param config - Configuration object to validate
|
||||
* @returns Array of validation errors
|
||||
*/
|
||||
function validateAgainstSchema(config, schema) {
|
||||
const errors = [];
|
||||
function validateAgainstSchema(config: AssetConfig): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// Basic structure validation
|
||||
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
|
||||
if (config.icon) {
|
||||
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)) {
|
||||
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
|
||||
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');
|
||||
errors.push({ message: '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');
|
||||
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
|
||||
if (config.splash) {
|
||||
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)) {
|
||||
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)) {
|
||||
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
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {Array} Array of missing file errors
|
||||
* @param config - Configuration object
|
||||
* @returns Array of missing file errors
|
||||
*/
|
||||
function validateSourceFiles(config) {
|
||||
const errors = [];
|
||||
const requiredFiles = new Set();
|
||||
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);
|
||||
@@ -100,7 +121,7 @@ function validateSourceFiles(config) {
|
||||
requiredFiles.forEach(file => {
|
||||
const filePath = path.join(PROJECT_ROOT, file);
|
||||
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
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {Array} Array of directory validation errors
|
||||
* @param config - Configuration object
|
||||
* @returns Array of directory validation errors
|
||||
*/
|
||||
function validateTargetDirectories(config) {
|
||||
const errors = [];
|
||||
const targetDirs = new Set();
|
||||
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);
|
||||
@@ -129,9 +150,9 @@ function validateTargetDirectories(config) {
|
||||
const parentDir = path.dirname(dirPath);
|
||||
|
||||
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()) {
|
||||
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
|
||||
* @param {string} configPath - Path to configuration file
|
||||
* @returns {boolean} True if validation passes
|
||||
* @param configPath - Path to configuration file
|
||||
* @returns True if validation passes
|
||||
*/
|
||||
function validateConfiguration(configPath) {
|
||||
function validateConfiguration(configPath: string): boolean {
|
||||
console.log('🔍 Validating TimeSafari asset configuration...');
|
||||
console.log(`📁 Config file: ${configPath}`);
|
||||
console.log(`📁 Project root: ${PROJECT_ROOT}`);
|
||||
@@ -159,7 +180,7 @@ function validateConfiguration(configPath) {
|
||||
console.log('✅ Schema file loaded successfully');
|
||||
|
||||
// Perform validations
|
||||
const schemaErrors = validateAgainstSchema(config, schema);
|
||||
const schemaErrors = validateAgainstSchema(config);
|
||||
const fileErrors = validateSourceFiles(config);
|
||||
const dirErrors = validateTargetDirectories(config);
|
||||
|
||||
@@ -179,13 +200,13 @@ function validateConfiguration(configPath) {
|
||||
} else {
|
||||
console.error('❌ Validation failed with the following errors:');
|
||||
allErrors.forEach((error, index) => {
|
||||
console.error(` ${index + 1}. ${error}`);
|
||||
console.error(` ${index + 1}. ${error.message}`);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Validation failed:', error.message);
|
||||
console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -193,7 +214,7 @@ function validateConfiguration(configPath) {
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
function main() {
|
||||
function main(): void {
|
||||
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
@@ -201,7 +222,7 @@ function main() {
|
||||
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(' - Specify path: tsx scripts/assets-validator.ts path/to/config.json');
|
||||
console.log(' - Generate config: npm run assets:config');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -215,4 +236,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories };
|
||||
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories, AssetConfig };
|
||||
Reference in New Issue
Block a user