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
text/typescript
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'}`);
}
};