UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

864 lines (750 loc) 26.8 kB
/** * Purge System Module for Kira CLI * * This module provides functions for cleaning up generated components, * adapted from the standalone purge-system.js for integration with the Kira CLI. */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const chalk = require('chalk'); const { isFeminine, getDefiniteArticle, getIndefiniteArticle, toPlural } = require('./french-language-utils'); // File paths are relative to the project root const PROJECT_ROOT = path.resolve(process.cwd()); /** * Get all generated components in the project * @returns {Promise<Array>} Array of component objects */ async function getGeneratedComponents() { const components = []; // Detect Angular components const angularComponentsPath = path.join(PROJECT_ROOT, 'front/src/app/pages/admin/settings'); if (fs.existsSync(angularComponentsPath)) { const dirs = fs.readdirSync(angularComponentsPath).filter(dir => { const dirPath = path.join(angularComponentsPath, dir); return fs.statSync(dirPath).isDirectory(); }); for (const dir of dirs) { const component = { name: dir, type: 'Fullstack', frontend: true, backend: false, files: [], angularPath: path.join(angularComponentsPath, dir) }; // Check if component files exist const componentFiles = fs.readdirSync(component.angularPath); component.files = componentFiles.map(file => path.join(component.angularPath, file)); // Check if the backend exists const modelPath = path.join(PROJECT_ROOT, 'back/app/Models', `${pascalCase(dir)}.php`); if (fs.existsSync(modelPath)) { component.backend = true; component.modelName = pascalCase(dir); // Add backend files to the list component.files.push(modelPath); const controllerPath = path.join(PROJECT_ROOT, 'back/app/Http/Controllers/Api', `${pascalCase(dir)}Controller.php`); if (fs.existsSync(controllerPath)) { component.files.push(controllerPath); } } components.push(component); } } return components; } /** * Backup important system files before purging * @returns {Promise<void>} */ async function backupSystemFiles() { console.log(chalk.blue('Backing up system files...')); const systemFiles = [ 'front/src/app/app.routes.ts', 'front/src/app/paths.ts', 'front/src/app/shared/components/sidebar/sidebar.component.ts', 'back/routes/api.php', 'back/app/Providers/CrudBindingServiceProvider.php' ]; const backupDir = path.join(PROJECT_ROOT, '.kira-backups', 'system-backup'); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } for (const file of systemFiles) { const sourcePath = path.join(PROJECT_ROOT, file); if (fs.existsSync(sourcePath)) { const backupPath = path.join(backupDir, file.replace(/\//g, '_')); fs.copyFileSync(sourcePath, backupPath); } } } /** * Restore system files after purge * @returns {Promise<void>} */ async function restoreSystemFiles() { console.log(chalk.blue('Restoring system files...')); const backupDir = path.join(PROJECT_ROOT, '.kira-backups', 'system-backup'); if (!fs.existsSync(backupDir)) return; const systemFiles = [ 'front_src_app_app.routes.ts', 'front_src_app_paths.ts', 'front_src_app_shared_components_sidebar_sidebar.component.ts', 'back_routes_api.php', 'back_app_Providers_CrudBindingServiceProvider.php' ]; for (const file of systemFiles) { const backupPath = path.join(backupDir, file); if (fs.existsSync(backupPath)) { const originalPath = path.join(PROJECT_ROOT, file.replace(/_/g, '/')); // Restore file based on type if (file.includes('app.routes.ts')) { await restoreCleanAppRoutes(originalPath); } else if (file.includes('paths.ts')) { await restoreCleanPaths(originalPath); } else if (file.includes('sidebar.component.ts')) { await restoreCleanSidebar(originalPath); } else if (file.includes('api.php')) { await restoreCleanApiRoutes(originalPath); } else if (file.includes('CrudBindingServiceProvider.php')) { await restoreCleanServiceProvider(originalPath); } } } } /** * Purge Angular components with transaction support * @param {Object} transaction - Transaction manager instance * @returns {Promise<void>} */ async function purgeAngularComponents(transaction) { console.log(chalk.blue('Removing Angular components...')); const componentsPath = path.join(PROJECT_ROOT, 'front/src/app/pages/admin/settings'); if (fs.existsSync(componentsPath)) { const dirs = fs.readdirSync(componentsPath).filter(dir => { const dirPath = path.join(componentsPath, dir); return fs.statSync(dirPath).isDirectory(); }); for (const dir of dirs) { const dirPath = path.join(componentsPath, dir); // Add delete operations to the transaction if provided if (transaction) { const files = fs.readdirSync(dirPath).map(file => path.join(dirPath, file)); for (const file of files) { await transaction.addDelete(file); } } else { fs.rmSync(dirPath, { recursive: true, force: true }); } console.log(chalk.red(` ❌ ${dir}`)); } } // Remove YAML configuration files const frontDir = path.join(PROJECT_ROOT, 'front'); if (fs.existsSync(frontDir)) { const yamlFiles = fs.readdirSync(frontDir).filter(file => file.endsWith('.crud.yaml') || file.endsWith('.crud.yml')); for (const file of yamlFiles) { const filePath = path.join(frontDir, file); // Add to transaction if provided if (transaction) { await transaction.addDelete(filePath); } else { fs.unlinkSync(filePath); } console.log(chalk.red(` ❌ ${file}`)); } } } /** * Purge Laravel components with transaction support * @param {Object} transaction - Transaction manager instance * @returns {Promise<void>} */ async function purgeLaravelComponents(transaction) { console.log(chalk.blue('Removing Laravel components...')); const backPath = path.join(PROJECT_ROOT, 'back'); // Remove models (keep User.php) const modelsPath = path.join(backPath, 'app/Models'); if (fs.existsSync(modelsPath)) { const models = fs.readdirSync(modelsPath).filter(file => file.endsWith('.php') && file !== 'User.php' ); for (const model of models) { const modelPath = path.join(modelsPath, model); // Add to transaction if provided if (transaction) { await transaction.addDelete(modelPath); } else { fs.unlinkSync(modelPath); } console.log(chalk.red(` ❌ Model: ${model}`)); } } // Remove repositories const repoPath = path.join(backPath, 'app/Repositories'); if (fs.existsSync(repoPath)) { // Add directories to transaction if provided if (transaction) { const repoDirs = ['', 'Interfaces']; for (const dir of repoDirs) { const repoSubPath = dir ? path.join(repoPath, dir) : repoPath; if (fs.existsSync(repoSubPath)) { const files = fs.readdirSync(repoSubPath).filter(file => file.endsWith('.php')); for (const file of files) { await transaction.addDelete(path.join(repoSubPath, file)); } } } } else { fs.rmSync(repoPath, { recursive: true, force: true }); } console.log(chalk.red(' ❌ Repositories')); } // Remove services const servicesPath = path.join(backPath, 'app/Services'); if (fs.existsSync(servicesPath)) { // Add directories to transaction if provided if (transaction) { const serviceDirs = ['', 'Interfaces']; for (const dir of serviceDirs) { const serviceSubPath = dir ? path.join(servicesPath, dir) : servicesPath; if (fs.existsSync(serviceSubPath)) { const files = fs.readdirSync(serviceSubPath).filter(file => file.endsWith('.php')); for (const file of files) { await transaction.addDelete(path.join(serviceSubPath, file)); } } } } else { fs.rmSync(servicesPath, { recursive: true, force: true }); } console.log(chalk.red(' ❌ Services')); } // Remove API controllers const controllersPath = path.join(backPath, 'app/Http/Controllers/Api'); if (fs.existsSync(controllersPath)) { // Add directories to transaction if provided if (transaction) { const files = fs.readdirSync(controllersPath).filter(file => file.endsWith('.php')); for (const file of files) { await transaction.addDelete(path.join(controllersPath, file)); } } else { fs.rmSync(controllersPath, { recursive: true, force: true }); } console.log(chalk.red(' ❌ API Controllers')); } // Remove resources (keep UserResource) const resourcesPath = path.join(backPath, 'app/Http/Resources'); if (fs.existsSync(resourcesPath)) { const resources = fs.readdirSync(resourcesPath).filter(file => file.endsWith('.php') && !file.includes('UserResource') ); for (const resource of resources) { const resourcePath = path.join(resourcesPath, resource); // Add to transaction if provided if (transaction) { await transaction.addDelete(resourcePath); } else { fs.unlinkSync(resourcePath); } console.log(chalk.red(` ❌ Resource: ${resource}`)); } } // Remove requests (keep Auth) const requestsPath = path.join(backPath, 'app/Http/Requests'); if (fs.existsSync(requestsPath)) { const dirs = fs.readdirSync(requestsPath).filter(dir => { const dirPath = path.join(requestsPath, dir); return fs.statSync(dirPath).isDirectory() && dir !== 'Auth'; }); for (const dir of dirs) { const dirPath = path.join(requestsPath, dir); // Add to transaction if provided if (transaction) { const files = fs.readdirSync(dirPath).map(file => path.join(dirPath, file)); for (const file of files) { await transaction.addDelete(file); } } else { fs.rmSync(dirPath, { recursive: true, force: true }); } console.log(chalk.red(` ❌ Requests: ${dir}`)); } } // Remove model parameters const parametersPath = path.join(backPath, 'app/ModelParameters'); if (fs.existsSync(parametersPath)) { // Add to transaction if provided if (transaction) { const files = fs.readdirSync(parametersPath).filter(file => file.endsWith('.php')); for (const file of files) { await transaction.addDelete(path.join(parametersPath, file)); } } else { fs.rmSync(parametersPath, { recursive: true, force: true }); } console.log(chalk.red(' ❌ Model Parameters')); } // Remove migrations (keep base migrations) const migrationsPath = path.join(backPath, 'database/migrations'); if (fs.existsSync(migrationsPath)) { const migrations = fs.readdirSync(migrationsPath).filter(file => file.endsWith('.php') && !file.includes('create_users_table') && !file.includes('create_password_resets_table') && !file.includes('create_failed_jobs_table') && !file.includes('create_personal_access_tokens_table') ); for (const migration of migrations) { const migrationPath = path.join(migrationsPath, migration); // Add to transaction if provided if (transaction) { await transaction.addDelete(migrationPath); } else { fs.unlinkSync(migrationPath); } console.log(chalk.red(` ❌ Migration: ${migration}`)); } } } /** * Purge a specific component with transaction support * @param {Object} component - The component to purge * @param {Object} transaction - Transaction manager instance * @returns {Promise<void>} */ async function purgeSpecificComponent(component, transaction) { const article = getDefiniteArticle(component.name); const isFem = isFeminine(component.name); console.log(chalk.blue(`Suppression ${isFem ? 'de la' : 'du'} composant ${component.name}...`)); // Remove Angular component if (component.frontend && fs.existsSync(component.angularPath)) { // Add files to transaction if provided if (transaction) { const files = fs.readdirSync(component.angularPath).map(file => path.join(component.angularPath, file) ); for (const file of files) { await transaction.addDelete(file); } } else { fs.rmSync(component.angularPath, { recursive: true, force: true }); } console.log(chalk.red(` ❌ Frontend: ${component.name}`)); } // Remove Laravel files if (component.backend && component.modelName) { await purgeLaravelComponentByName(component.modelName, transaction); } // Clean routes and bindings await cleanSpecificComponentRoutes(component.name); if (component.modelName) { await cleanSpecificComponentBindings(component.modelName); } } /** * Purge a Laravel component by name with transaction support * @param {string} modelName - The model name to purge * @param {Object} transaction - Transaction manager instance * @returns {Promise<void>} */ async function purgeLaravelComponentByName(modelName, transaction) { const backPath = path.join(PROJECT_ROOT, 'back'); // Model const modelPath = path.join(backPath, `app/Models/${modelName}.php`); if (fs.existsSync(modelPath)) { if (transaction) { await transaction.addDelete(modelPath); } else { fs.unlinkSync(modelPath); } } // Repository const repoPath = path.join(backPath, `app/Repositories/${modelName}Repository.php`); const repoInterfacePath = path.join(backPath, `app/Repositories/Interfaces/${modelName}RepositoryInterface.php`); if (fs.existsSync(repoPath)) { if (transaction) { await transaction.addDelete(repoPath); } else { fs.unlinkSync(repoPath); } } if (fs.existsSync(repoInterfacePath)) { if (transaction) { await transaction.addDelete(repoInterfacePath); } else { fs.unlinkSync(repoInterfacePath); } } // Service const servicePath = path.join(backPath, `app/Services/${modelName}Service.php`); const serviceInterfacePath = path.join(backPath, `app/Services/Interfaces/${modelName}ServiceInterface.php`); if (fs.existsSync(servicePath)) { if (transaction) { await transaction.addDelete(servicePath); } else { fs.unlinkSync(servicePath); } } if (fs.existsSync(serviceInterfacePath)) { if (transaction) { await transaction.addDelete(serviceInterfacePath); } else { fs.unlinkSync(serviceInterfacePath); } } // Controller const controllerPath = path.join(backPath, `app/Http/Controllers/Api/${modelName}Controller.php`); if (fs.existsSync(controllerPath)) { if (transaction) { await transaction.addDelete(controllerPath); } else { fs.unlinkSync(controllerPath); } } // Resources const resourcePath = path.join(backPath, `app/Http/Resources/${modelName}Resource.php`); const collectionPath = path.join(backPath, `app/Http/Resources/${modelName}Collection.php`); if (fs.existsSync(resourcePath)) { if (transaction) { await transaction.addDelete(resourcePath); } else { fs.unlinkSync(resourcePath); } } if (fs.existsSync(collectionPath)) { if (transaction) { await transaction.addDelete(collectionPath); } else { fs.unlinkSync(collectionPath); } } // Requests const requestDir = path.join(backPath, `app/Http/Requests/${modelName}`); if (fs.existsSync(requestDir)) { if (transaction) { // Add all files in the directory to the transaction const requestFiles = fs.readdirSync(requestDir).map(file => path.join(requestDir, file)); for (const file of requestFiles) { await transaction.addDelete(file); } } else { fs.rmSync(requestDir, { recursive: true, force: true }); } } // Migration const migrationsPath = path.join(backPath, 'database/migrations'); if (fs.existsSync(migrationsPath)) { const tableName = modelName.toLowerCase() + 's'; const migrations = fs.readdirSync(migrationsPath).filter(file => file.includes(`create_${tableName}_table`) ); for (const migration of migrations) { const migrationPath = path.join(migrationsPath, migration); if (transaction) { await transaction.addDelete(migrationPath); } else { fs.unlinkSync(migrationPath); } } } // Model Parameters const paramPath = path.join(backPath, `app/ModelParameters/${modelName}Parameters.php`); if (fs.existsSync(paramPath)) { if (transaction) { await transaction.addDelete(paramPath); } else { fs.unlinkSync(paramPath); } } console.log(chalk.red(` ❌ Backend: ${modelName}`)); } /** * Clean Angular routes * @returns {Promise<void>} */ async function cleanAngularRoutes() { console.log(chalk.blue('Cleaning Angular routes...')); await restoreCleanAppRoutes(path.join(PROJECT_ROOT, 'front/src/app/app.routes.ts')); await restoreCleanPaths(path.join(PROJECT_ROOT, 'front/src/app/paths.ts')); } /** * Clean Angular sidebar * @returns {Promise<void>} */ async function cleanAngularSidebar() { console.log(chalk.blue('Cleaning Angular sidebar...')); await restoreCleanSidebar(path.join(PROJECT_ROOT, 'front/src/app/shared/components/sidebar/sidebar.component.ts')); } /** * Clean Laravel routes * @returns {Promise<void>} */ async function cleanLaravelRoutes() { console.log(chalk.blue('Cleaning Laravel routes...')); await restoreCleanApiRoutes(path.join(PROJECT_ROOT, 'back/routes/api.php')); } /** * Clean service bindings * @returns {Promise<void>} */ async function cleanServiceBindings() { console.log(chalk.blue('Cleaning service bindings...')); await restoreCleanServiceProvider(path.join(PROJECT_ROOT, 'back/app/Providers/CrudBindingServiceProvider.php')); } /** * Clean routes for a specific component * @param {string} componentName - Name of the component * @returns {Promise<void>} */ async function cleanSpecificComponentRoutes(componentName) { // Clean Angular-specific routes const pathsFile = path.join(PROJECT_ROOT, 'front/src/app/paths.ts'); const routesFile = path.join(PROJECT_ROOT, 'front/src/app/app.routes.ts'); const sidebarFile = path.join(PROJECT_ROOT, 'front/src/app/shared/components/sidebar/sidebar.component.ts'); const constName = componentName.toUpperCase().replace(/[^A-Z0-9]/g, '_'); // Remove from paths.ts if (fs.existsSync(pathsFile)) { let content = fs.readFileSync(pathsFile, 'utf8'); content = content.replace(new RegExp(`\\s*${constName}\\s*=\\s*[^,]+,?`, 'g'), ''); fs.writeFileSync(pathsFile, content); } // Remove from app.routes.ts if (fs.existsSync(routesFile)) { let content = fs.readFileSync(routesFile, 'utf8'); const componentClass = pascalCase(componentName) + 'Component'; content = content.replace(new RegExp(`import.*${componentClass}.*from.*\\n`, 'g'), ''); content = content.replace(new RegExp(`\\s*\\{[^}]*path:\\s*Paths\\.${constName}[^}]*\\},?\\n`, 'g'), ''); fs.writeFileSync(routesFile, content); } // Remove from sidebar if (fs.existsSync(sidebarFile)) { let content = fs.readFileSync(sidebarFile, 'utf8'); // First, check if we need to remove an entry if (content.includes(`this.paths.${constName}`)) { content = content.replace(new RegExp(`\\s*\\{[^}]*this\\.paths\\.${constName}[^}]*\\},?\\n`, 'g'), ''); // If we're modifying the menuConfig, make sure we preserve the Dashboard entry const ngOnInitRegex = /ngOnInit\s*\(\)\s*\{[\s\S]*?\}/; const ngOnInitMatch = content.match(ngOnInitRegex); if (ngOnInitMatch && !content.includes('label: \'Dashboard\'')) { // Replace with our preserved version if Dashboard is missing const preservedNgOnInit = `ngOnInit() { this.menuConfig = [ { label: 'Dashboard', link: Paths.DASHBOARD, icon: 'chart-line', }, ]; }`; content = content.replace(ngOnInitRegex, preservedNgOnInit); } fs.writeFileSync(sidebarFile, content); } } } /** * Clean bindings for a specific component * @param {string} modelName - Name of the model * @returns {Promise<void>} */ async function cleanSpecificComponentBindings(modelName) { if (!modelName) return; const providerPath = path.join(PROJECT_ROOT, 'back/app/Providers/CrudBindingServiceProvider.php'); if (fs.existsSync(providerPath)) { let content = fs.readFileSync(providerPath, 'utf8'); content = content.replace(new RegExp(`\\s*\\$this->app->bind\\(.*${modelName}.*\\);\\n`, 'g'), ''); fs.writeFileSync(providerPath, content); } } /** * Drop generated database tables * @returns {Promise<void>} */ async function dropGeneratedTables() { console.log(chalk.blue('Dropping generated tables...')); try { const backPath = path.join(PROJECT_ROOT, 'back'); process.chdir(backPath); // Reset migrations (keep base migrations) execSync('php artisan migrate:reset --force', { stdio: 'pipe' }); execSync('php artisan migrate --force', { stdio: 'pipe' }); console.log(chalk.green(' ✅ Tables reset successfully')); // Return to original directory process.chdir(PROJECT_ROOT); } catch (error) { console.log(chalk.yellow(' ⚠️ Could not automatically reset tables')); console.log(chalk.yellow(' 💡 Run manually: cd back && php artisan migrate:reset && php artisan migrate')); throw error; } } /** * Restore clean app routes file * @param {string} filePath - Path to the app.routes.ts file * @returns {Promise<void>} */ async function restoreCleanAppRoutes(filePath) { const cleanContent = `import { Routes } from '@angular/router'; import { Paths } from './paths'; import { LoginComponent } from './pages/security/login/login.component'; export const routes: Routes = [ { path: '', redirectTo: Paths.LOGIN, pathMatch: 'full' }, { path: Paths.LOGIN, component: LoginComponent }, // Routes générées automatiquement seront ajoutées ici ]; `; fs.writeFileSync(filePath, cleanContent); } /** * Restore clean paths file * @param {string} filePath - Path to the paths.ts file * @returns {Promise<void>} */ async function restoreCleanPaths(filePath) { const cleanContent = `export enum Paths { LOGIN = 'login', DASHBOARD = 'dashboard', // Paths générés automatiquement seront ajoutés ici } `; fs.writeFileSync(filePath, cleanContent); } /** * Restore clean sidebar file * @param {string} filePath - Path to the sidebar.component.ts file * @returns {Promise<void>} */ async function restoreCleanSidebar(filePath) { // Keep the structure but remove generated entries if (fs.existsSync(filePath)) { let content = fs.readFileSync(filePath, 'utf8'); // Remove all menu entries containing "this.paths." (generated entries) // except for the Dashboard entry // First, let's extract the ngOnInit function const ngOnInitRegex = /ngOnInit\s*\(\)\s*\{[\s\S]*?\}/; const ngOnInitMatch = content.match(ngOnInitRegex); if (ngOnInitMatch) { // Replace the entire ngOnInit function with our preserved version const preservedNgOnInit = `ngOnInit() { this.menuConfig = [ { label: 'Dashboard', link: Paths.DASHBOARD, icon: 'chart-line', guarded: true, }, ]; }`; content = content.replace(ngOnInitRegex, preservedNgOnInit); } fs.writeFileSync(filePath, content); } } /** * Restore clean API routes file * @param {string} filePath - Path to the api.php file * @returns {Promise<void>} */ async function restoreCleanApiRoutes(filePath) { const cleanContent = `<?php use Illuminate\\Http\\Request; use Illuminate\\Support\\Facades\\Route; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- | | Here is where you can register API routes for your application. These | routes are loaded by the RouteServiceProvider and all of them will | be assigned to the "api" middleware group. Make something great! | */ Route::middleware('auth:sanctum')->get('/user', function (Request $request) { return $request->user(); }); // Routes générées automatiquement seront ajoutées ici `; fs.writeFileSync(filePath, cleanContent); } /** * Restore clean service provider file * @param {string} filePath - Path to the CrudBindingServiceProvider.php file * @returns {Promise<void>} */ async function restoreCleanServiceProvider(filePath) { const cleanContent = `<?php namespace App\\Providers; use Illuminate\\Support\\ServiceProvider; class CrudBindingServiceProvider extends ServiceProvider { /** * Register services. */ public function register(): void { // CRUD Bindings - Auto-generated // Les bindings seront ajoutés automatiquement ici par le générateur CRUD } /** * Bootstrap services. */ public function boot(): void { // } } `; fs.writeFileSync(filePath, cleanContent); } /** * Convert string to PascalCase * @param {string} str - The string to convert * @returns {string} The PascalCase string */ function pascalCase(str) { return str .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')) .replace(/^./, (s) => s.toUpperCase()); } /** * Convert string to kebab-case * @param {string} str - The string to convert * @returns {string} The kebab-case string */ function kebabCase(str) { return str .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .toLowerCase(); } module.exports = { getGeneratedComponents, purgeAngularComponents, purgeLaravelComponents, purgeSpecificComponent, purgeLaravelComponentByName, cleanAngularRoutes, cleanAngularSidebar, cleanLaravelRoutes, cleanServiceBindings, cleanSpecificComponentRoutes, cleanSpecificComponentBindings, dropGeneratedTables, restoreCleanAppRoutes, restoreCleanPaths, restoreCleanSidebar, restoreCleanApiRoutes, restoreCleanServiceProvider, backupSystemFiles, restoreSystemFiles, pascalCase, kebabCase };