UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

397 lines (337 loc) 11.9 kB
/** * Base Generator * Abstract base class for all CRUD generators */ const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); const ora = require('ora'); const yaml = require('js-yaml'); const TransactionManager = require('../utils/transaction-manager'); /** * Base Generator class */ class BaseGenerator { /** * Create a new generator * @param {Object} options - Generator options */ constructor(options = {}) { this.options = options; this.config = null; this.transaction = null; this.spinner = null; this.verbose = options.verbose || false; this.failFast = options.failFast !== false; // Default to true this.createBackups = options.createBackups !== false; // Default to true this.errors = []; this.warnings = []; } /** * Load a configuration file * @param {string} filePath - Path to the configuration file * @returns {Promise<Object>} Parsed configuration */ async loadConfig(filePath) { try { const content = await fs.readFile(filePath, 'utf8'); if (filePath.endsWith('.yml') || filePath.endsWith('.yaml')) { this.config = yaml.load(content); } else if (filePath.endsWith('.json')) { this.config = JSON.parse(content); } else { throw new Error('Unsupported file format. Use YAML or JSON.'); } return this.config; } catch (error) { throw new Error(`Failed to load configuration: ${error.message}`); } } /** * Initialize the generator * @returns {Promise<void>} */ async initialize() { // Initialize transaction manager this.transaction = new TransactionManager({ verbose: this.verbose, createBackups: this.createBackups }); await this.transaction.init(); if (this.verbose) { console.log(chalk.blue('Generator initialized')); } } /** * Generate files from templates * @param {string} templateDir - Directory containing templates * @param {string} outputDir - Directory to write generated files * @param {Object} variables - Variables to substitute in templates * @returns {Promise<void>} */ async generateFromTemplates(templateDir, outputDir, variables) { try { // Ensure output directory exists await this.transaction.addMkdir(outputDir); // Get all template files const templateFiles = await fs.readdir(templateDir); for (const templateFile of templateFiles) { // Skip directories and non-template files if (templateFile.endsWith('.template')) { const templatePath = path.join(templateDir, templateFile); const templateContent = await fs.readFile(templatePath, 'utf8'); // Get output file name by removing .template extension const outputFileName = templateFile.replace('.template', ''); const outputFilePath = path.join(outputDir, outputFileName); // Replace variables in template const processedContent = this.processTemplate(templateContent, variables); // Add write operation to transaction await this.transaction.addWrite(outputFilePath, processedContent); if (this.verbose) { console.log(chalk.gray(`Added template processing: ${templateFile} -> ${outputFilePath}`)); } } } } catch (error) { throw new Error(`Failed to generate from templates: ${error.message}`); } } /** * Process a template by replacing variables * @param {string} template - Template content * @param {Object} variables - Variables to substitute * @returns {string} Processed template */ processTemplate(template, variables) { let processed = template; // Process arrays first (for formFields, etc.) for (const [key, value] of Object.entries(variables)) { if (Array.isArray(value)) { const regex = new RegExp(`{{#${key}}}([\\s\\S]*?){{\\/${key}}}`, 'g'); processed = processed.replace(regex, (match, content) => { return value.map(item => { let itemContent = content; if (typeof item === 'object') { // Process nested conditionals in item (e.g., {{#isText}}...{{/isText}}) for (const [itemKey, itemValue] of Object.entries(item)) { if (typeof itemValue === 'boolean') { const condRegex = new RegExp(`{{#${itemKey}}}([\\s\\S]*?){{\\/${itemKey}}}`, 'g'); if (itemValue) { // Keep the content if the variable is true itemContent = itemContent.replace(condRegex, (match, condContent) => condContent); } else { // Remove the content if the variable is false itemContent = itemContent.replace(condRegex, ''); } } } // Replace item properties for (const [itemKey, itemValue] of Object.entries(item)) { if (typeof itemValue === 'string' || typeof itemValue === 'number' || typeof itemValue === 'boolean') { const itemRegex = new RegExp(`{{\\s*${itemKey}\\s*}}`, 'g'); itemContent = itemContent.replace(itemRegex, itemValue); } } } else { // Replace {{.}} with the item value const dotRegex = new RegExp(`{{\\s*\\.\\s*}}`, 'g'); itemContent = itemContent.replace(dotRegex, item); } return itemContent; }).join(''); }); } } // Simple variable replacement for (const [key, value] of Object.entries(variables)) { if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g'); processed = processed.replace(regex, value); } } // Process conditional sections {{#variable}}...{{/variable}} for (const [key, value] of Object.entries(variables)) { if (typeof value === 'boolean') { const regex = new RegExp(`{{#${key}}}([\\s\\S]*?){{\\/${key}}}`, 'g'); if (value) { // Keep the content if the variable is true processed = processed.replace(regex, (match, content) => content); } else { // Remove the content if the variable is false processed = processed.replace(regex, ''); } } } // Clean up any remaining Mustache tags (important for nested conditionals that may have been missed) // First, clean remaining conditional blocks const remainingConditionals = /{{#\w+}}[\s\S]*?{{\/\w+}}/g; processed = processed.replace(remainingConditionals, ''); // Then clean any remaining variables const remainingVars = /{{[\s\S]*?}}/g; processed = processed.replace(remainingVars, ''); return processed; } /** * Execute the generation process * @returns {Promise<boolean>} Whether the generation succeeded */ async execute() { this.spinner = ora('Starting generation process...').start(); try { // Validation step this.spinner.text = 'Validating configuration...'; await this.validate(); // Preparation step this.spinner.text = 'Preparing generation...'; await this.prepare(); // Generation step this.spinner.text = 'Generating files...'; await this.generate(); // Execute all file operations this.spinner.text = 'Executing file operations...'; const success = await this.transaction.execute(); if (success) { // Post-processing step this.spinner.text = 'Running post-processing...'; await this.postProcess(); this.spinner.succeed('Generation completed successfully'); // Display summary this.displaySummary(); // Clean up backups await this.transaction.cleanup(); return true; } else { this.spinner.fail('Generation failed'); return false; } } catch (error) { this.spinner.fail('Generation failed'); console.error(chalk.red(`Error: ${error.message}`)); // Log the error this.errors.push(error.message); // Roll back if necessary if (this.transaction && !this.transaction.rolledBack) { await this.transaction.rollback(); } return false; } } /** * Validate the configuration * @returns {Promise<void>} */ async validate() { // To be implemented by subclasses throw new Error('Method not implemented'); } /** * Prepare for generation * @returns {Promise<void>} */ async prepare() { // To be implemented by subclasses throw new Error('Method not implemented'); } /** * Generate the code * @returns {Promise<void>} */ async generate() { // To be implemented by subclasses throw new Error('Method not implemented'); } /** * Run post-processing after generation * @returns {Promise<void>} */ async postProcess() { // To be implemented by subclasses // Default implementation does nothing } /** * Display a summary of the generation process */ displaySummary() { const summary = this.transaction.getSummary(); console.log(chalk.blue('\nGeneration Summary:')); console.log(chalk.blue('-------------------')); console.log(`Total operations: ${summary.operations}`); if (summary.operationsByType.write) { console.log(`Files created/modified: ${summary.operationsByType.write}`); } if (summary.operationsByType.delete) { console.log(`Files deleted: ${summary.operationsByType.delete}`); } if (summary.operationsByType.mkdir) { console.log(`Directories created: ${summary.operationsByType.mkdir}`); } if (this.warnings.length > 0) { console.log(chalk.yellow('\nWarnings:')); this.warnings.forEach((warning, index) => { console.log(chalk.yellow(` ${index + 1}. ${warning}`)); }); } if (this.errors.length > 0) { console.log(chalk.red('\nErrors:')); this.errors.forEach((error, index) => { console.log(chalk.red(` ${index + 1}. ${error}`)); }); } } /** * Add a warning message * @param {string} message - Warning message */ addWarning(message) { this.warnings.push(message); if (this.verbose) { console.log(chalk.yellow(`Warning: ${message}`)); } } /** * Add an error message * @param {string} message - Error message */ addError(message) { this.errors.push(message); if (this.verbose) { console.error(chalk.red(`Error: ${message}`)); } if (this.failFast) { throw new Error(message); } } /** * Check if a file exists * @param {string} filePath - Path to the file * @returns {Promise<boolean>} Whether the file exists */ async fileExists(filePath) { try { const stats = await fs.stat(filePath); return stats.isFile(); } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; } } /** * Check if a directory exists * @param {string} dirPath - Path to the directory * @returns {Promise<boolean>} Whether the directory exists */ async directoryExists(dirPath) { try { const stats = await fs.stat(dirPath); return stats.isDirectory(); } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; } } } module.exports = BaseGenerator;