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