UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

483 lines (412 loc) 17.8 kB
#!/usr/bin/env node /** * Script dédié à la génération CRUD * Ce script est conçu pour être utilisé directement dans une installation globale */ const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); const { exec } = require('child_process'); const util = require('util'); const execPromise = util.promisify(exec); const yaml = require('js-yaml'); const ora = require('ora'); const boxen = require('boxen'); const settingsManager = require('./utils/settings-manager'); // Importer directement les modules nécessaires const TransactionManager = require('./utils/transaction-manager'); const projectConfig = require('./utils/project-config'); /** * Exécuter la génération CRUD * @param {string} configPath - Chemin vers le fichier de configuration * @param {boolean} [showPathsOnly=false] - Afficher uniquement les chemins sans générer */ async function runGenerator(configPath, showPathsOnly = false) { console.log(chalk.blue(`\nGénération CRUD à partir de: ${configPath}\n`)); // Initialiser le spinner const spinner = ora('Initialisation de la génération CRUD...').start(); try { // Vérifier si le fichier existe try { await fs.access(configPath); } catch (error) { spinner.fail(`Le fichier de configuration '${configPath}' n'existe pas.`); return; } // Lire le fichier de configuration const configContent = await fs.readFile(configPath, 'utf8'); let config; // Parser selon le format if (configPath.endsWith('.json')) { config = JSON.parse(configContent); } else if (configPath.endsWith('.yml') || configPath.endsWith('.yaml')) { config = yaml.load(configContent); } else { spinner.fail('Format de fichier non pris en charge. Utilisez .yml, .yaml ou .json'); return; } // Initialiser la transaction const transaction = new TransactionManager({ verbose: false, createBackups: true }); await transaction.init(); // Obtenir le projet actif let activeProject; try { activeProject = await projectConfig.getActiveProject(); console.log(chalk.blue(`\nUtilisation du projet: ${chalk.bold(activeProject.name)}\n`)); // Afficher les chemins qui seront utilisés const laravelPath = projectConfig.getProjectPath(activeProject, 'laravel'); const angularPath = projectConfig.getProjectPath(activeProject, 'angular'); console.log(chalk.blue(`Chemins de génération:`)); console.log(chalk.green(`- Backend: ${laravelPath}`)); console.log(chalk.green(`- Frontend: ${angularPath}`)); console.log(); // Vérifier si les chemins existent try { await fs.access(laravelPath); } catch (accessError) { console.log(chalk.yellow(`\nAttention: Le chemin backend '${laravelPath}' n'existe pas ou n'est pas accessible.`)); console.log(chalk.yellow(`Vous pouvez configurer les chemins avec la commande 'kira' puis en sélectionnant '\u2699\ufe0f Application Settings'.\n`)); } // Si on veut seulement afficher les chemins, on arrête ici if (showPathsOnly) { spinner.stop(); return; } } catch (error) { spinner.fail('Aucun projet configuré. Veuillez configurer un projet avec la commande "kira config"'); return; } // Détecter si le modèle est féminin ou masculin pour la UI const modelName = config.model?.name; if (!modelName) { spinner.fail('Nom du modèle non trouvé dans la configuration'); return; } spinner.text = 'Génération des composants backend...'; // Générer les composants backend (Laravel) try { // Créer le fichier ModelParameters await createModelParametersFile(configPath, activeProject); // Déterminer l'architecture à utiliser (classique ou avancée) const configContent = await fs.readFile(configPath, 'utf8'); let configObject; if (configPath.endsWith('.json')) { configObject = JSON.parse(configContent); } else { configObject = yaml.load(configContent); } // Déterminer quelle commande Artisan exécuter en fonction de l'architecture const architecture = configObject.backend?.architecture || 'advanced'; const laravelPath = projectConfig.getProjectPath(activeProject, 'laravel'); let artisanCommand; if (architecture === 'classic') { artisanCommand = `cd "${laravelPath}" && php artisan make:crud:classic ${modelName} --parameters --force`; } else { artisanCommand = `cd "${laravelPath}" && php artisan make:crud ${modelName} --parameters --force`; } // Exécuter la commande Artisan await execPromise(artisanCommand); // Exécuter les migrations Laravel spinner.text = 'Exécution des migrations Laravel...'; try { // S'assurer d'utiliser le chemin correct pour Laravel const currentLaravelPath = projectConfig.getProjectPath(activeProject, 'laravel'); // Vérifier que le chemin existe try { await fs.access(currentLaravelPath); } catch (accessError) { throw new Error(`Le chemin Laravel '${currentLaravelPath}' n'existe pas ou n'est pas accessible.`); } // Exécuter la migration const migrateCommand = `cd "${currentLaravelPath}" && php artisan migrate`; console.log(chalk.blue(`\nExécution de: ${migrateCommand}`)); await execPromise(migrateCommand); spinner.text = 'Migrations exécutées avec succès'; } catch (migrationError) { spinner.warn("Attention: Les migrations n'ont pas pu être exécutées automatiquement"); console.log(chalk.yellow(`\nErreur: ${migrationError.message}`)); console.log(chalk.yellow(`\nVous devrez exécuter manuellement les migrations.\n`)); } } catch (backendError) { spinner.warn(`Erreur lors de la génération backend: ${backendError.message}`); } // Générer les composants frontend (Angular) spinner.text = 'Génération des composants frontend...'; try { const { generateFromConfig } = require('./generate-custom'); const angularPath = projectConfig.getProjectPath(activeProject, 'angular'); const settingsPath = activeProject.angular.settingsPath || 'app/pages/admin/settings'; const outputPath = path.join(angularPath, 'src', settingsPath); await generateFromConfig(configPath, { output: outputPath, }); spinner.text = 'Composants frontend générés avec succès'; // Mise à jour des fichiers de routing Angular spinner.text = 'Mise à jour des fichiers de routing Angular...'; try { const { updateAngularRoutes } = require('./utils/angular-routes-updater'); const componentDir = path.join(outputPath, modelName.toLowerCase()); await updateAngularRoutes(config, componentDir, angularPath); // Vérification supplémentaire pour s'assurer que les routes sont correctement ajoutées try { const { fixAngularRoutes } = require('./fix-angular-routes'); const routesFile = path.join(angularPath, 'src/app/app.routes.ts'); await fixAngularRoutes(routesFile); spinner.text = 'Fichiers de routing mis à jour et vérifiés avec succès'; } catch (fixRoutesError) { spinner.info("Note: La vérification des routes n'a pas pu être effectuée. Vous pouvez utiliser npm run fix-routes pour corriger les routes."); } } catch (routingError) { spinner.warn("Attention: Les fichiers de routing n'ont pas pu être mis à jour automatiquement"); console.log(chalk.yellow("\nVous devrez mettre à jour manuellement les fichiers de routing Angular\n")); } } catch (frontendError) { spinner.warn(`Erreur lors de la génération frontend: ${frontendError.message}`); } // Enregistrer la transaction await transaction.execute(); spinner.succeed('Génération CRUD terminée avec succès'); console.log( boxen( `${chalk.green.bold("✓")} Génération réussie pour ${chalk.cyan( configPath.split("/").pop() )}\n\n` + `${chalk.bold("Backend:")} composants Laravel générés\n` + `${chalk.bold("Frontend:")} composants Angular générés\n\n` + `${chalk.gray("Exécutez l'application pour voir vos modifications.")}`, { padding: 1, margin: 1, borderStyle: "round", borderColor: "green", } ) ); } catch (error) { spinner.fail('Génération échouée'); console.error(chalk.red(`\nErreur: ${error.message}\n`)); } } /** * Créer le fichier ModelParameters pour Laravel * @param {string} configPath - Chemin vers le fichier de configuration * @param {Object} activeProject - Configuration du projet actif */ async function createModelParametersFile(configPath, activeProject) { try { // Lire le fichier de configuration const configContent = await fs.readFile(configPath, 'utf8'); let config; // Parser selon le format if (configPath.endsWith('.json')) { config = JSON.parse(configContent); } else { config = yaml.load(configContent); } // Importer les fonctions nécessaires const { pluralize, kebabCase } = require('./utils/string-utils'); // Extraire les informations du modèle const modelName = config.model.name; const fields = config.model.fields || []; // Créer le répertoire ModelParameters s'il n'existe pas const laravelPath = projectConfig.getProjectPath(activeProject, 'laravel'); const modelParametersDir = path.join(laravelPath, 'app/ModelParameters'); await fs.mkdir(modelParametersDir, { recursive: true }); // Construire les données pour le fichier ModelParameters const fillable = fields.map(field => field.name); // Ajouter les clés étrangères des relations à fillable if (config.model.relationships && config.model.relationships.length > 0) { config.model.relationships.forEach(rel => { if (rel.type === 'belongsTo' || rel.type === 'morphTo') { const foreignKey = rel.foreignKey || `${rel.name}_id`; if (!fillable.includes(foreignKey)) { fillable.push(foreignKey); } } }); } const searchableFields = fields.filter(field => !field.noSearch).map(field => field.name); // Construire les règles de validation const validationRules = {}; fields.forEach(field => { let rules = []; if (!field.nullable) { rules.push('required'); } else { rules.push('nullable'); } // Ajouter une validation de type appropriée switch (field.type) { case 'string': rules.push('string'); rules.push('max:255'); break; case 'text': rules.push('string'); break; case 'integer': rules.push('integer'); break; case 'decimal': case 'float': rules.push('numeric'); break; case 'boolean': rules.push('boolean'); break; case 'date': rules.push('date'); break; } // Ajouter les validations de la configuration if (field.validations && field.validations.length > 0) { field.validations.forEach(validation => { if (validation === 'unique') { rules.push(`unique:${config.model.tableName || pluralize(modelName.toLowerCase())},${field.name}`); } else if (validation === 'exists') { rules.push(`exists:${config.model.tableName || pluralize(modelName.toLowerCase())},${field.name}`); } else if (validation === 'in' && field.options) { const values = Array.isArray(field.options) ? field.options.join(',') : field.options; rules.push(`in:${values}`); } else if (!rules.includes(validation)) { rules.push(validation); } }); } validationRules[field.name] = rules.join('|'); }); // Construire les relations const relationships = {}; if (config.model.relationships && config.model.relationships.length > 0) { config.model.relationships.forEach(rel => { relationships[rel.name] = { model: rel.relatedModel || rel.model, type: rel.type, required: rel.required || false, displayField: rel.displayField || 'name' }; // Ajouter des règles de validation pour les clés étrangères if ((rel.type === 'belongsTo' || rel.type === 'morphTo') && rel.required) { const foreignKey = rel.foreignKey || `${rel.name}_id`; const relatedModel = rel.relatedModel || rel.model; if (relatedModel) { const relatedTable = pluralize(relatedModel.toLowerCase()); let rules = []; rules.push('required'); rules.push('integer'); rules.push(`exists:${relatedTable},id`); validationRules[foreignKey] = rules.join('|'); } } }); } // Construire l'objet parameters final const parameters = { fillable, searchableFields, validationRules, relationships }; // Générer le contenu PHP const phpContent = `<?php\n\nreturn ${generatePHPArrayString(parameters)};\n`; // Écrire le fichier const parametersFilePath = path.join(modelParametersDir, `${modelName}Parameters.php`); await fs.writeFile(parametersFilePath, phpContent); console.log(chalk.green(`✓ Fichier ModelParameters créé pour ${modelName}`)); } catch (error) { console.error(chalk.red(`Erreur lors de la création du fichier ModelParameters: ${error.message}`)); throw error; } } /** * Générer une représentation PHP d'un tableau * @param {Object} obj - L'objet à convertir * @param {number} indent - Niveau d'indentation * @returns {string} Chaîne représentant un tableau PHP */ function generatePHPArrayString(obj, indent = 0) { const spaces = ' '.repeat(indent * 4); const innerSpaces = ' '.repeat((indent + 1) * 4); if (Array.isArray(obj)) { if (obj.length === 0) return '[]'; const items = obj.map(item => { if (typeof item === 'string') { return `${innerSpaces}'${item}'`; } else if (typeof item === 'number') { return `${innerSpaces}${item}`; } else if (typeof item === 'boolean') { return `${innerSpaces}${item ? 'true' : 'false'}`; } else if (item === null) { return `${innerSpaces}null`; } else { return `${innerSpaces}${generatePHPArrayString(item, indent + 1)}`; } }).join(',\n'); return `[\n${items}\n${spaces}]`; } else if (typeof obj === 'object' && obj !== null) { const entries = Object.entries(obj); if (entries.length === 0) return '[]'; const items = entries.map(([key, value]) => { const valueStr = typeof value === 'string' ? `'${value}'` : (typeof value === 'number' ? value : (typeof value === 'boolean' ? (value ? 'true' : 'false') : (value === null ? 'null' : generatePHPArrayString(value, indent + 1)))); return `${innerSpaces}'${key}' => ${valueStr}`; }).join(',\n'); return `[\n${items}\n${spaces}]`; } else { return obj; } } // Point d'entrée du script if (require.main === module) { // Utiliser Commander pour gérer les arguments proprement const { program } = require('commander'); program .description('Générateur CRUD pour Laravel et Angular') .argument('[configPath]', 'Chemin vers le fichier de configuration YAML ou JSON') .option('--show-paths', 'Afficher les chemins utilisés pour la génération') .action(async (configPath, options) => { // Afficher les chemins configurés si demandé if (options.showPaths) { const settingsManager = require('./utils/settings-manager'); await settingsManager.displayPaths(); if (!configPath) { return; } try { // Afficher les chemins qui seraient utilisés pour ce fichier de configuration spécifique console.log(chalk.blue(`\nAffichage des chemins pour: ${configPath}\n`)); await runGenerator(path.resolve(configPath), true); // true = show paths only } catch (error) { console.error(chalk.red(`Erreur: ${error.message}`)); } return; } if (!configPath) { console.error(chalk.red('Erreur: Veuillez spécifier un chemin vers un fichier de configuration')); console.log(chalk.yellow('\nUtilisation: kira-generate <chemin_config> [options]')); console.log(chalk.yellow('Options:')); console.log(chalk.yellow(' --show-paths Afficher les chemins utilisés pour la génération')); process.exit(1); } runGenerator(path.resolve(configPath)) .catch(error => { console.error(chalk.red(`Erreur: ${error.message}`)); process.exit(1); }); }); program.parse(); } // Exporter la fonction pour utilisation dans d'autres modules module.exports = { runGenerator };