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:
Matthew Raymer
2025-08-14 09:08:06 +00:00
parent 76749a097d
commit 495a94827a
5 changed files with 666 additions and 66 deletions

View File

@@ -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 };

View File

@@ -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 };