UNPKG

ruch

Version:

Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.

552 lines (462 loc) 21.7 kB
import fs from 'fs-extra'; import path from 'path'; import readline from 'readline'; import type { FileSystem } from '../utils/file-operations'; import type { Logger } from '../utils/logging'; import { logError, logSuccess, logInfo, logWarning } from '../utils/logging'; import { getExistingDomains } from '../utils/domain-analyzer'; import { getMswSetupTemplate, getMswBrowserTemplate, getMswServerTemplate, getMswHandlersTemplate, getMswHandlersIndexTemplate, getMswMocksTemplate, getMswSetupTestsTemplate } from '../templates/msw'; import { analyzeDomain } from '../utils/domain-file-analyzer'; import { generateDynamicMswHandlers, generateDynamicMockData } from '../templates/msw-dynamic'; /** * Pure function to validate MSW init preconditions */ const validateMswInitPreconditions = async ( fileSystem: FileSystem, mswSetupPath: string ): Promise<{ canInit: boolean; reason?: string }> => { const mswExists = await fileSystem.exists(mswSetupPath); if (mswExists) { return { canInit: false, reason: 'MSW is already initialized in this project' }; } return { canInit: true }; }; /** * Pure function to ask for user confirmation */ const askForConfirmation = (question: string): Promise<boolean> => { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(`${question} (y/N): `, (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); }); }); }; /** * Create MSW directory structure and configuration files */ const createMswFiles = async ( fileSystem: FileSystem, mswPath: string, setupFilePath: string, browserFilePath: string, serverFilePath: string, setupTestsFilePath: string, setupContent: string, browserContent: string, serverContent: string, setupTestsContent: string ): Promise<void> => { await fileSystem.ensureDir(mswPath); await fileSystem.ensureDir(path.join(mswPath, 'handlers')); await fileSystem.writeFile(setupFilePath, setupContent, 'utf8'); await fileSystem.writeFile(browserFilePath, browserContent, 'utf8'); await fileSystem.writeFile(serverFilePath, serverContent, 'utf8'); await fileSystem.writeFile(setupTestsFilePath, setupTestsContent, 'utf8'); }; /** * Initialize MSW configuration in the project */ export const initMsw = async ( fileSystem: FileSystem = fs, log: Logger = require('../utils/logger').logger, projectRoot: string = process.cwd() ): Promise<void> => { const srcPath = path.join(projectRoot, 'src'); const mswPath = path.join(srcPath, 'mocks'); const setupFilePath = path.join(mswPath, 'setup.ts'); const browserFilePath = path.join(mswPath, 'browser.ts'); const serverFilePath = path.join(mswPath, 'server.ts'); const setupTestsFilePath = path.join(srcPath, 'setupTests.ts'); try { const validation = await validateMswInitPreconditions(fileSystem, setupFilePath); if (!validation.canInit) { logWarning(log, validation.reason || 'Cannot initialize MSW'); return; } logInfo(log, 'Initializing MSW (Mock Service Worker) configuration...'); const setupContent = getMswSetupTemplate(); const browserContent = getMswBrowserTemplate(); const serverContent = getMswServerTemplate(); const setupTestsContent = getMswSetupTestsTemplate(); // Create empty handlers index initially const handlersIndexPath = path.join(mswPath, 'handlers', 'index.ts'); const emptyHandlersIndex = getMswHandlersIndexTemplate([]); await createMswFiles( fileSystem, mswPath, setupFilePath, browserFilePath, serverFilePath, setupTestsFilePath, setupContent, browserContent, serverContent, setupTestsContent ); // Create the handlers index file await fileSystem.writeFile(handlersIndexPath, emptyHandlersIndex, 'utf8'); logSuccess(log, 'MSW initialized successfully!'); logInfo(log, `📁 MSW configuration created at: ${mswPath}`); logInfo(log, '📦 Install MSW with: bun add -D msw'); logInfo(log, '🔧 Generate browser worker: bunx msw init public/ --save'); logInfo(log, '💡 Use "ruch msw handlers generate" to create domain handlers'); } catch (error) { logError(log, `Error initializing MSW: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Generate MSW handlers for all domains */ export const generateMswHandlers = async ( fileSystem: FileSystem = fs, log: Logger = require('../utils/logger').logger, projectRoot: string = process.cwd() ): Promise<void> => { const srcPath = path.join(projectRoot, 'src'); const mswPath = path.join(srcPath, 'mocks'); const handlersPath = path.join(mswPath, 'handlers'); try { const mswExists = await fileSystem.exists(mswPath); if (!mswExists) { logError(log, 'MSW not initialized. Run "ruch msw init" first.'); return; } logInfo(log, 'Generating MSW handlers for all domains...'); const domains = await getExistingDomains(fileSystem, projectRoot); if (domains.length === 0) { logWarning(log, 'No domains found. Create domains first with "ruch create <domain-name>".'); return; } let handlersGenerated = 0; for (const domain of domains) { const handlerFilePath = path.join(handlersPath, `${domain.name}.ts`); const mocksFolderPath = path.join(srcPath, 'domains', domain.name, 'mocks'); // Check if handler already exists const handlerExists = await fileSystem.exists(handlerFilePath); if (!handlerExists) { const handlerContent = getMswHandlersTemplate(domain.name); await fileSystem.writeFile(handlerFilePath, handlerContent, 'utf8'); handlersGenerated++; // Create domain mocks folder await fileSystem.ensureDir(mocksFolderPath); logInfo(log, `✅ Created handler: ${domain.name}.ts`); } else { logInfo(log, `⏭️ Handler already exists: ${domain.name}.ts`); } } // Update setup files to include all handlers const setupFilePath = path.join(mswPath, 'setup.ts'); const browserFilePath = path.join(mswPath, 'browser.ts'); const serverFilePath = path.join(mswPath, 'server.ts'); const handlersIndexPath = path.join(handlersPath, 'index.ts'); const domainNames = domains.map(d => d.name); const setupContent = getMswSetupTemplate(); const browserContent = getMswBrowserTemplate(); const serverContent = getMswServerTemplate(); const handlersIndexContent = getMswHandlersIndexTemplate(domainNames); await fileSystem.writeFile(setupFilePath, setupContent, 'utf8'); await fileSystem.writeFile(browserFilePath, browserContent, 'utf8'); await fileSystem.writeFile(serverFilePath, serverContent, 'utf8'); await fileSystem.writeFile(handlersIndexPath, handlersIndexContent, 'utf8'); logSuccess(log, `MSW handlers generated successfully!`); logInfo(log, `📁 Generated ${handlersGenerated} new handler(s)`); logInfo(log, `📁 Updated setup files with ${domains.length} domain(s)`); logInfo(log, '💡 Use "ruch msw mocks generate" to create mock data'); } catch (error) { logError(log, `Error generating MSW handlers: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Generate mock data for all domains */ export const generateMswMocks = async ( fileSystem: FileSystem = fs, log: Logger = require('../utils/logger').logger, projectRoot: string = process.cwd() ): Promise<void> => { const srcPath = path.join(projectRoot, 'src'); try { const mswPath = path.join(srcPath, 'mocks'); const mswExists = await fileSystem.exists(mswPath); if (!mswExists) { logError(log, 'MSW not initialized. Run "ruch msw init" first.'); return; } logInfo(log, 'Generating mock data for all domains...'); const domains = await getExistingDomains(fileSystem, projectRoot); if (domains.length === 0) { logWarning(log, 'No domains found. Create domains first with "ruch create <domain-name>".'); return; } let mocksGenerated = 0; for (const domain of domains) { const domainMocksPath = path.join(srcPath, 'domains', domain.name, 'mocks'); const mockDataFilePath = path.join(domainMocksPath, 'mockData.ts'); // Check if mock data already exists const mockDataExists = await fileSystem.exists(mockDataFilePath); if (!mockDataExists) { await fileSystem.ensureDir(domainMocksPath); const mockContent = getMswMocksTemplate(domain.name); await fileSystem.writeFile(mockDataFilePath, mockContent, 'utf8'); mocksGenerated++; logInfo(log, `✅ Created mock data: ${domain.name}/mocks/mockData.ts`); } else { logInfo(log, `⏭️ Mock data already exists: ${domain.name}/mocks/mockData.ts`); } } logSuccess(log, `Mock data generated successfully!`); logInfo(log, `📁 Generated ${mocksGenerated} new mock file(s)`); logInfo(log, '💡 Edit mock data files to match your domain entities'); logInfo(log, '💡 Use "ruch msw update" to regenerate after schema changes'); } catch (error) { logError(log, `Error generating mock data: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Update MSW configuration and handlers for a specific domain */ export const updateMswDomain = async ( domainName: string, fileSystem: FileSystem = fs, log: Logger = require('../utils/logger').logger, projectRoot: string = process.cwd(), skipConfirmation: boolean = false ): Promise<void> => { const srcPath = path.join(projectRoot, 'src'); const mswPath = path.join(srcPath, 'mocks'); const handlersPath = path.join(mswPath, 'handlers'); try { const mswExists = await fileSystem.exists(mswPath); if (!mswExists) { logError(log, 'MSW not initialized. Run "ruch msw init" first.'); return; } // Check if domain exists const domains = await getExistingDomains(fileSystem, projectRoot); const targetDomain = domains.find(d => d.name === domainName); if (!targetDomain) { logError(log, `Domain "${domainName}" not found. Available domains: ${domains.map(d => d.name).join(', ')}`); return; } logInfo(log, `Updating MSW configuration for domain "${domainName}"...`); // Create backups of the specific handler and related files const handlerFilePath = path.join(handlersPath, `${domainName}.ts`); const domainMocksPath = path.join(srcPath, 'domains', domainName, 'mocks', 'mockData.ts'); const handlersIndexPath = path.join(handlersPath, 'index.ts'); const timestamp = Date.now(); // Backup handler if it exists if (await fileSystem.exists(handlerFilePath)) { const backupHandlerPath = `${handlerFilePath}.backup.${timestamp}`; const handlerContent = await fileSystem.readFile(handlerFilePath, 'utf8'); await fileSystem.writeFile(backupHandlerPath, handlerContent, 'utf8'); logInfo(log, `📁 Handler backup: ${path.basename(backupHandlerPath)}`); } // Backup mock data if it exists if (await fileSystem.exists(domainMocksPath)) { const backupMockPath = `${domainMocksPath}.backup.${timestamp}`; const mockContent = await fileSystem.readFile(domainMocksPath, 'utf8'); await fileSystem.writeFile(backupMockPath, mockContent, 'utf8'); logInfo(log, `📁 Mock data backup: ${path.basename(backupMockPath)}`); } logWarning(log, `⚠️ WARNING: This will regenerate MSW files for domain "${domainName}"`); logInfo(log, `📁 Backups created with timestamp: ${timestamp}`); logInfo(log, '💡 This will overwrite handler and mock data files'); // Ask for confirmation unless skipped (for tests) if (!skipConfirmation) { const confirmed = await askForConfirmation('Do you want to continue?'); if (!confirmed) { logInfo(log, '❌ Operation cancelled by user'); return; } } // Analyze the domain to get real structure const domainPath = path.join(srcPath, 'domains', domainName); logInfo(log, `🔍 Analyzing domain structure...`); try { const analysis = await analyzeDomain(domainPath, domainName); logInfo(log, `📊 Found ${analysis.entities.length} entities, ${analysis.adapters.length} adapters, ${analysis.ports.length} ports`); // Generate dynamic handler based on real domain structure const handlerContent = generateDynamicMswHandlers(analysis); await fileSystem.writeFile(handlerFilePath, handlerContent, 'utf8'); // Ensure domain mocks folder exists const domainMocksFolderPath = path.join(srcPath, 'domains', domainName, 'mocks'); await fileSystem.ensureDir(domainMocksFolderPath); // Generate dynamic mock data based on real entities const mockContent = generateDynamicMockData(analysis); await fileSystem.writeFile(domainMocksPath, mockContent, 'utf8'); // Log what was found and generated if (analysis.entities.length > 0) { logInfo(log, `📝 Entities analyzed: ${analysis.entities.map(e => e.name).join(', ')}`); } if (analysis.adapters.length > 0) { logInfo(log, `🔌 Adapters analyzed: ${analysis.adapters.map(a => a.name).join(', ')}`); } if (analysis.ports.length > 0) { const totalMethods = analysis.ports.reduce((sum, p) => sum + p.methods.length, 0); logInfo(log, `⚙️ Generated ${totalMethods} API endpoint(s) based on port methods`); } // Important note about customization needed logWarning(log, '⚠️ IMPORTANT: MSW handlers are based on your Port interfaces.'); logInfo(log, '📝 You must customize the endpoints to match your real API:'); logInfo(log, ' • Update API_BASE to your actual base URL'); logInfo(log, ' • Modify endpoint paths for each method'); logInfo(log, ' • Customize request/response handling logic'); logWarning(log, '⚠️ IMPORTANT: Mock data is generated from your Entity definitions.'); logInfo(log, '📝 You should customize the mock data to match your business needs:'); logInfo(log, ' • Update sample data with realistic values'); logInfo(log, ' • Add domain-specific business logic'); logInfo(log, ' • Customize validation and error scenarios'); } catch (error) { logWarning(log, `⚠️ Could not analyze domain structure: ${error instanceof Error ? error.message : 'Unknown error'}`); logInfo(log, '📄 Falling back to generic templates...'); // Fallback to generic templates const handlerContent = getMswHandlersTemplate(domainName); await fileSystem.writeFile(handlerFilePath, handlerContent, 'utf8'); const domainMocksFolderPath = path.join(srcPath, 'domains', domainName, 'mocks'); await fileSystem.ensureDir(domainMocksFolderPath); const mockContent = getMswMocksTemplate(domainName); await fileSystem.writeFile(domainMocksPath, mockContent, 'utf8'); } // Update handlers index to ensure it includes all domains const allDomains = await getExistingDomains(fileSystem, projectRoot); const domainNames = allDomains.map(d => d.name); const handlersIndexContent = getMswHandlersIndexTemplate(domainNames); await fileSystem.writeFile(handlersIndexPath, handlersIndexContent, 'utf8'); logSuccess(log, `MSW configuration updated for domain "${domainName}"!`); logInfo(log, `✅ Regenerated handler: ${domainName}.ts`); logInfo(log, `✅ Regenerated mock data: ${domainName}/mocks/mockData.ts`); logWarning(log, `🔄 IMPORTANT: If you had custom modifications, they were overwritten!`); logInfo(log, `📁 Backup files created with timestamp: ${timestamp}`); logInfo(log, `💡 To restore your custom changes:`); logInfo(log, ` • Check .backup.${timestamp} files in handlers/ and domain/mocks/`); logInfo(log, ` • Copy your custom endpoint paths and logic back to the new files`); logInfo(log, ` • Merge your custom mock data with the newly generated structure`); } catch (error) { logError(log, `Error updating MSW for domain "${domainName}": ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Update MSW configuration and handlers */ export const updateMsw = async ( fileSystem: FileSystem = fs, log: Logger = require('../utils/logger').logger, projectRoot: string = process.cwd(), skipConfirmation: boolean = false ): Promise<void> => { const srcPath = path.join(projectRoot, 'src'); const mswPath = path.join(srcPath, 'mocks'); try { const mswExists = await fileSystem.exists(mswPath); if (!mswExists) { logError(log, 'MSW not initialized. Run "ruch msw init" first.'); return; } logInfo(log, 'Checking for MSW updates...'); const domains = await getExistingDomains(fileSystem, projectRoot); const handlersPath = path.join(mswPath, 'handlers'); // Check for missing handlers const missingHandlers = []; for (const domain of domains) { const handlerFilePath = path.join(handlersPath, `${domain.name}.ts`); const handlerExists = await fileSystem.exists(handlerFilePath); if (!handlerExists) { missingHandlers.push(domain.name); } } if (missingHandlers.length === 0) { logInfo(log, 'MSW configuration is already up to date. No missing handlers found.'); return; } logInfo(log, `Found ${missingHandlers.length} missing handler(s):`); missingHandlers.forEach(handler => logInfo(log, ` - ${handler}`)); // Create backup of setup files const setupFilePath = path.join(mswPath, 'setup.ts'); const browserFilePath = path.join(mswPath, 'browser.ts'); const serverFilePath = path.join(mswPath, 'server.ts'); const timestamp = Date.now(); const backupSetupPath = `${setupFilePath}.backup.${timestamp}`; const backupBrowserPath = `${browserFilePath}.backup.${timestamp}`; const backupServerPath = `${serverFilePath}.backup.${timestamp}`; if (await fileSystem.exists(setupFilePath)) { const setupContent = await fileSystem.readFile(setupFilePath, 'utf8'); await fileSystem.writeFile(backupSetupPath, setupContent, 'utf8'); } if (await fileSystem.exists(browserFilePath)) { const browserContent = await fileSystem.readFile(browserFilePath, 'utf8'); await fileSystem.writeFile(backupBrowserPath, browserContent, 'utf8'); } if (await fileSystem.exists(serverFilePath)) { const serverContent = await fileSystem.readFile(serverFilePath, 'utf8'); await fileSystem.writeFile(backupServerPath, serverContent, 'utf8'); } logWarning(log, '⚠️ WARNING: This will update your MSW configuration files'); logInfo(log, `📁 Backups created with timestamp: ${timestamp}`); logInfo(log, '💡 Review the backups if you had custom modifications'); // Ask for confirmation unless skipped (for tests) if (!skipConfirmation) { const confirmed = await askForConfirmation('Do you want to continue?'); if (!confirmed) { logInfo(log, '❌ Operation cancelled by user'); // Remove backups since we're not proceeding await fileSystem.remove(backupSetupPath).catch(() => {}); await fileSystem.remove(backupBrowserPath).catch(() => {}); await fileSystem.remove(backupServerPath).catch(() => {}); return; } } // Generate missing handlers for (const handlerName of missingHandlers) { const handlerFilePath = path.join(handlersPath, `${handlerName}.ts`); const handlerContent = getMswHandlersTemplate(handlerName); await fileSystem.writeFile(handlerFilePath, handlerContent, 'utf8'); // Create domain mocks folder if it doesn't exist const domainMocksPath = path.join(srcPath, 'domains', handlerName, 'mocks'); await fileSystem.ensureDir(domainMocksPath); } // Update setup files const handlersIndexPath = path.join(handlersPath, 'index.ts'); const domainNames = domains.map(d => d.name); const setupContent = getMswSetupTemplate(); const browserContent = getMswBrowserTemplate(); const serverContent = getMswServerTemplate(); const handlersIndexContent = getMswHandlersIndexTemplate(domainNames); await fileSystem.writeFile(setupFilePath, setupContent, 'utf8'); await fileSystem.writeFile(browserFilePath, browserContent, 'utf8'); await fileSystem.writeFile(serverFilePath, serverContent, 'utf8'); await fileSystem.writeFile(handlersIndexPath, handlersIndexContent, 'utf8'); logSuccess(log, 'MSW configuration updated successfully!'); logInfo(log, `Added ${missingHandlers.length} handler(s) to MSW configuration`); logInfo(log, `Total domains: ${domains.length}`); logWarning(log, `🔄 IMPORTANT: If you had custom modifications, they were overwritten!`); logInfo(log, `📁 Backup files created with timestamp: ${timestamp}`); logInfo(log, `💡 To restore your custom changes:`); logInfo(log, ` • Check .backup.${timestamp} files in mocks/ folder`); logInfo(log, ` • Copy your custom configuration back to the new setup files`); } catch (error) { logError(log, `Error updating MSW: ${error instanceof Error ? error.message : 'Unknown error'}`); } };