kira-crud
Version:
Intelligent CRUD Generator for Laravel and Angular
397 lines (337 loc) • 11.9 kB
JavaScript
/**
* 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;