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.

1,464 lines (1,253 loc) 58.9 kB
import fs from 'fs-extra'; import path from 'path'; import chalk from 'chalk'; import type { Logger } from '../services/console-logger'; interface DomainStructure { entities: string; ports: string; services: string; adapters: string; hooks: string; ui: string; } interface RuchGuideConfig { project: { name: string; description: string; architecture: string; domains: Record<string, DomainStructure>; }; rules: { business_logic_isolation: string; use_ruch_cli: string; typescript: string; modularity: string; tests: string; comments: string; reactQuery: string; dependency_injection: string; naming: string; no_direct_service_imports: string; msw_testing: string; msw_customization: string; msw_workflow: string; msw_organization: string; no_function_mocks: string; no_local_hook_mocks: string; no_barrel_mocks: string; }; cli_commands: { create_domain: string; list_domains: string; update_context: string; regenerate_guide: string; msw_init: string; msw_handlers: string; msw_mocks: string; msw_update: string; msw_update_domain: string; }; workflow: { domain_creation: string[]; msw_setup: string[]; }; anti_patterns: { business_logic_in_components: string; manual_domain_creation: string; direct_service_imports: string; cross_domain_direct_imports: string; local_hook_mocks: string; function_mocking: string; barrel_file_mocks: string; }; examples: { port_example: string; adapter_example: string; service_example: string; hook_example: string; entity_example: string; component_example: string; test_service_example: string; test_adapter_example: string; test_hook_example: string; msw_handler_example: string; msw_mock_data_example: string; }; } /** * Analyzes the project structure to detect existing domains */ async function analyzeDomains(): Promise<Record<string, DomainStructure>> { const domainsPath = path.join(process.cwd(), 'src', 'domains'); const domains: Record<string, DomainStructure> = {}; try { const domainFolders = await fs.readdir(domainsPath); for (const domainFolder of domainFolders) { const domainPath = path.join(domainsPath, domainFolder); const stat = await fs.stat(domainPath); if (stat.isDirectory()) { domains[domainFolder] = { entities: `src/domains/${domainFolder}/entities`, ports: `src/domains/${domainFolder}/ports`, services: `src/domains/${domainFolder}/services`, adapters: `src/domains/${domainFolder}/adapters`, hooks: `src/domains/${domainFolder}/hooks`, ui: `src/domains/${domainFolder}/ui` }; } } } catch (error) { // If domains folder doesn't exist, return empty object console.warn(chalk.yellow('Warning: src/domains folder not found')); } return domains; } /** * Generates the ruch-guide.json configuration file */ async function generateGuideConfig(domains: Record<string, DomainStructure>): Promise<void> { const config: RuchGuideConfig = { project: { name: "Ruch Project", description: "A modular, hexagonal architecture-based React project using simplified hexagonal architecture with React contexts and React Query.", architecture: "Simplified Hexagonal Architecture", domains }, rules: { business_logic_isolation: "ALL business logic MUST be contained within domains - never in components, pages, utils, or shared hooks. Components handle only UI logic and user interactions.", use_ruch_cli: "Always use Ruch CLI to create domains: 'ruch create <domain-name>'. Never manually create domain folders. Use 'ruch context generate' after creating domains.", typescript: "Use TypeScript for all components, services, and interfaces with JSDoc comments for all public methods.", modularity: "Domains must interact only via ports and adapters. Cross-domain communication must go through well-defined ports.", tests: "Tests MUST be co-located with their respective files using .test.ts/.test.tsx extensions. Never create separate test directories.", comments: "All public methods and interfaces should have JSDoc comments explaining their purpose.", reactQuery: "Use React Query for data fetching and state management within hooks with consistent query keys per domain.", dependency_injection: "Use React Context via ServiceProvider for dependency injection. Never instantiate services directly.", naming: "Use PascalCase for components/classes, camelCase for functions/variables, kebab-case for files.", no_direct_service_imports: "Never import services directly in components - always use domain hooks.", msw_testing: "Use MSW for mocking APIs in tests with handlers organized by domain in dedicated mocks/ folders. NEVER mock functions or hooks - use MSW to mock API responses instead.", msw_customization: "CRITICAL: Generated MSW handlers and mock data are templates only and MUST be customized for your specific API and business logic.", msw_workflow: "Always follow MSW workflow: init -> generate handlers -> generate mocks -> CUSTOMIZE (critical step) -> test -> update when domains change.", msw_organization: "Store domain-specific MSW handlers and mock data in {domain}/mocks/ folders. Use global src/mocks/ only for server configuration and shared utilities.", no_function_mocks: "NEVER mock functions, hooks, or services. Use MSW to mock API responses. Minimize mock usage and prefer real implementations.", no_local_hook_mocks: "NEVER create local mock implementations of domain hooks in components. ALWAYS import actual domain hooks from their designated locations.", no_barrel_mocks: "NEVER create barrel export files (index.ts) with mock implementations. Keep barrel exports clean with only real exports." }, cli_commands: { create_domain: "ruch create <domain-name> - Create a new domain with hexagonal structure", list_domains: "ruch list - List all existing domains before creating new ones", update_context: "ruch context generate - Update ServiceContext after creating domains", regenerate_guide: "ruch guide-ai - Regenerate AI documentation when structure changes", msw_init: "ruch msw init - Initialize MSW configuration in the project", msw_handlers: "ruch msw handlers generate - Generate MSW handlers for all domains", msw_mocks: "ruch msw mocks generate - Generate mock data files for all domains", msw_update: "ruch msw update - Update MSW configuration with missing handlers", msw_update_domain: "ruch msw update [domain] - Update MSW configuration for a specific domain" }, workflow: { domain_creation: [ "Identify business capability", "Run: ruch create <domain-name>", "Define entities in entities/", "Define ports in ports/", "Implement services in services/", "Create adapters in adapters/", "Build hooks in hooks/", "Run: ruch context generate", "Write co-located tests" ], msw_setup: [ "Run: ruch msw init (initialize MSW configuration)", "Run: ruch msw handlers generate (generate handler templates)", "Run: ruch msw mocks generate (generate mock data templates)", "CUSTOMIZE handlers for your API endpoints (CRITICAL)", "CUSTOMIZE mock data with realistic business data (CRITICAL)", "Test your MSW setup with your application", "Run: ruch msw update when domains change" ] }, anti_patterns: { business_logic_in_components: "Never put business logic, validations, or calculations in React components or pages", manual_domain_creation: "Never manually create domain folders - always use 'ruch create <domain>'", direct_service_imports: "Never import services directly in components - always use domain hooks", cross_domain_direct_imports: "Never import services/entities directly between domains - use ports/adapters", local_hook_mocks: "Never create local mock implementations of domain hooks in components - import actual hooks", function_mocking: "Never mock functions or hooks - use MSW to mock API responses instead", barrel_file_mocks: "Never create barrel export files (index.ts) with mock implementations - keep them clean" }, examples: { port_example: "Write an interface to abstract domain behavior and enable cross-domain communication.", adapter_example: "Write an implementation that consumes an API and implements a port interface.", service_example: "Write business logic that uses ports for data access and contains all domain rules.", hook_example: "Write React hooks that combine services with React Query and expose data to components.", entity_example: "Define TypeScript interfaces for domain models with validation functions.", component_example: "Write React components that use domain hooks and handle only UI logic.", test_service_example: "Test services with mocked ports/repositories.", test_adapter_example: "Test adapters with MSW for realistic API mocking.", test_hook_example: "Test hooks with React Testing Library and mocked ServiceProvider.", msw_handler_example: "ALWAYS customize generated MSW handlers: update endpoints, add authentication, implement validation, create error scenarios.", msw_mock_data_example: "ALWAYS customize generated mock data: replace placeholder values, add realistic business data, implement domain validation rules." } }; const configPath = path.join(process.cwd(), 'ruch-guide.json'); await fs.writeFile(configPath, JSON.stringify(config, null, 2)); } /** * Generates the global GUIDE.md file */ async function generateGlobalGuide(domains: Record<string, DomainStructure>): Promise<void> { const domainList = Object.keys(domains).map(domain => `- **${domain}**: Domain managing ${domain.toLowerCase()} business logic`).join('\n'); const guideContent = `# Ruch Project Guide for AI Assistants ## Overview This project follows a Simplified Hexagonal Architecture for React projects, organized intuitively to resemble a classic React structure while using contexts and React Query, with tests placed next to the files they test. ## Complete Project Structure \`\`\` src/ ├── components/ # Shared React components │ ├── ui/ # Reusable UI components │ │ ├── Button.tsx │ │ ├── Button.test.tsx # Co-located test │ │ └── ... │ └── layouts/ # Shared layouts │ ├── pages/ # React pages (route components) │ ├── Home.tsx │ ├── Home.test.tsx # Co-located test │ └── ... │ ├── hooks/ # Shared React hooks │ ├── useForm.ts │ ├── useForm.test.ts # Co-located test │ └── ... │ ├── domain/ # All business domains │ ├── user/ # User domain │ │ ├── entities/ # Data models │ │ │ ├── User.ts │ │ │ └── User.test.ts │ │ │ │ │ ├── ports/ # Adapter interfaces │ │ │ └── UserPorts.ts │ │ │ │ │ ├── services/ # Business services │ │ │ ├── UserService.ts │ │ │ └── UserService.test.ts │ │ │ │ │ ├── adapters/ # Concrete implementations │ │ │ ├── UserApiAdapter.ts │ │ │ └── UserApiAdapter.test.ts │ │ │ │ │ ├── hooks/ # Domain-specific hooks │ │ │ ├── useUser.ts │ │ │ └── useUser.test.ts │ │ │ │ │ ├── ui/ # Domain-specific UI │ │ │ ├── UserView.tsx │ │ │ └── UserView.test.tsx │ │ │ │ │ └── mocks/ # Domain-specific MSW mocks │ │ ├── handlers.ts # MSW request handlers │ │ └── mockData.ts # Mock data for testing │ │ │ └── product/ # Other domains... │ ├── context/ # Global React contexts │ ├── ServiceContext.tsx │ └── ServiceContext.test.tsx │ ├── utils/ # Utilities and helpers │ ├── formatters.ts │ ├── formatters.test.ts │ └── ... │ ├── mocks/ # Global MSW configuration only │ ├── server.ts # MSW server for Node.js │ └── browser.ts # MSW worker for browser │ ├── assets/ # Static resources │ ├── images/ │ └── styles/ │ ├── App.tsx # Application entry point ├── App.test.tsx # App component test ├── index.tsx # Initialization file └── setupTests.ts # Global test configuration \`\`\` ## Detected Domains ${domainList || '- No domains detected'} ## Guidelines for AI Tools ### 1. Business Logic Isolation (CRITICAL) - **ALL business logic MUST be contained within domains** - never outside - No business logic in components, pages, or shared hooks - No business logic in utils or helpers folders - Components should only handle UI logic and user interactions - Business rules, validations, and calculations belong in domain services ### 2. Use Ruch CLI Commands - **Always use Ruch CLI to create new domains**: \`ruch create <domain-name>\` - Use \`ruch list\` to see existing domains before creating new ones - Use \`ruch context generate\` to update ServiceContext after creating domains - Use \`ruch guide-ai\` to regenerate documentation when structure changes - **Never manually create domain folders** - always use the CLI ### 3. Hexagonal Architecture Compliance - Domains must interact only via ports and adapters - Keep business logic in services within domains - Services must be independent of React - Use hooks to connect React to services - Never import services directly in components - always use domain hooks - Cross-domain communication must go through well-defined ports ### 4. TypeScript Requirements - Use TypeScript for all components, services, and interfaces - Define strict types for entities with JSDoc comments - All public methods and interfaces must have JSDoc documentation - Use discriminated unions for entity states when applicable ### 5. Co-located Tests (CRITICAL) - Tests MUST be co-located with their respective files using .test.ts/.test.tsx extensions - Each entity, service, adapter, and hook must have corresponding test files - Use MSW for mocking APIs in tests - Place tests next to the files they test, never in separate test folders - Test business logic thoroughly in service tests ### 6. React Query Integration - Use React Query for data fetching and state management within hooks - Define consistent query keys per domain using constants - Handle loading, error, and success states properly - Implement optimistic updates with mutations - Query keys should follow the pattern: \`[domain, type, ...identifiers]\` ### 7. Dependency Injection via React Context - Use React Context via ServiceProvider for dependency injection - Create service mocks for tests - Never instantiate services directly in components or hooks - Update ServiceContext when adding new domains ### 8. Mock Organization Strategy - **Domain-specific mocks**: Place MSW handlers and mock data in {domain}/mocks/ - **Global MSW setup**: Keep server/browser configuration in src/mocks/ - **Mock data isolation**: Each domain manages its own test data - **Handler organization**: Group request handlers by domain for maintainability ## Ruch CLI Commands Usage ### Creating New Domains When you need to add business functionality, always start by creating a domain: \`\`\`bash # List existing domains first ruch list # Create a new domain (with all hexagonal components) ruch create order # The CLI will generate: # - src/domain/order/entities/ # - src/domain/order/ports/ # - src/domain/order/services/ # - src/domain/order/adapters/ # - src/domain/order/hooks/ # - src/domain/order/ui/ # Update ServiceContext after creating domains ruch context generate # Generate fresh AI documentation ruch guide-ai \`\`\` ### Available Ruch Commands - \`ruch create <domain>\` - Create a new domain with hexagonal structure - \`ruch delete <domain>\` - Delete an existing domain - \`ruch list\` - List all existing domains - \`ruch context init\` - Initialize empty service context - \`ruch context generate\` - Generate service context with all domains - \`ruch context update\` - Update context with missing domains - \`ruch http-client\` - Generate HTTP client configuration - \`ruch guide-ai\` - Generate AI-friendly documentation - \`ruch visualize\` - Visualize domain dependencies ### Domain Creation Workflow 1. **Identify business capability**: "I need to handle user orders" 2. **Create domain**: \`ruch create order\` 3. **Define entities**: Add business models in \`entities/\` 4. **Define ports**: Add interfaces in \`ports/\` 5. **Implement services**: Add business logic in \`services/\` 6. **Create adapters**: Add data access in \`adapters/\` 7. **Build hooks**: Connect to React in \`hooks/\` 8. **Update context**: \`ruch context generate\` 9. **Test everything**: Write co-located tests ## Complete Code Examples ### 1. Entity with Validation \`\`\`typescript // src/domain/user/entities/User.ts export interface User { id: string; name: string; email: string; role: 'admin' | 'user'; createdAt: string; } // Validation example export function validateUser(user: Partial<User>): string[] { const errors: string[] = []; if (!user.name?.trim()) errors.push('Name is required'); if (!user.email?.includes('@')) errors.push('Valid email is required'); return errors; } \`\`\` ### 2. Port Interface \`\`\`typescript // src/domain/user/ports/UserPorts.ts import { User } from '../entities/User'; export interface UserPorts { getUser(id: string): Promise<User>; updateUser(user: User): Promise<User>; getUserList(): Promise<User[]>; } \`\`\` ### 3. Service with Business Logic \`\`\`typescript // src/domain/user/services/UserService.ts import { User } from '../entities/User'; import { UserPorts } from '../ports/UserPorts'; export class UserService { constructor(private userPorts: UserPorts) {} async getUser(id: string): Promise<User> { return this.userPorts.getUser(id); } async updateUser(user: User): Promise<User> { return this.userPorts.updateUser(user); } async getUserList(): Promise<User[]> { return this.userPorts.getUserList(); } // Business logic example async promoteToAdmin(userId: string): Promise<User> { const user = await this.getUser(userId); if (user.role === 'admin') { throw new Error('User is already an admin'); } const updatedUser = { ...user, role: 'admin' as const }; return this.updateUser(updatedUser); } } \`\`\` ### 4. API Adapter Implementation \`\`\`typescript // src/domain/user/adapters/UserApiAdapter.ts import { User } from '../entities/User'; import { UserPorts } from '../ports/UserPorts'; export class UserApiAdapter implements UserPorts { private baseUrl: string; constructor(baseUrl: string = '/api/users') { this.baseUrl = baseUrl; } async getUser(id: string): Promise<User> { const response = await fetch(\`\${this.baseUrl}/\${id}\`); if (!response.ok) { throw new Error(\`Error fetching user: \${response.statusText}\`); } return await response.json(); } async updateUser(user: User): Promise<User> { const response = await fetch(\`\${this.baseUrl}/\${user.id}\`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(user) }); if (!response.ok) { throw new Error(\`Error updating user: \${response.statusText}\`); } return await response.json(); } async getUserList(): Promise<User[]> { const response = await fetch(this.baseUrl); if (!response.ok) { throw new Error(\`Error fetching users: \${response.statusText}\`); } return await response.json(); } } \`\`\` ### 5. Complete ServiceContext with Dependency Injection \`\`\`typescript // src/context/ServiceContext.tsx import React, { createContext, useContext } from 'react'; import { UserService } from '../domain/user/services/UserService'; import { UserApiAdapter } from '../domain/user/adapters/UserApiAdapter'; import { ProductService } from '../domain/product/services/ProductService'; import { ProductApiAdapter } from '../domain/product/adapters/ProductApiAdapter'; // Base configuration const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || '/api'; // Create adapters const userRepository = new UserApiAdapter(\`\${API_BASE_URL}/users\`); const productRepository = new ProductApiAdapter(\`\${API_BASE_URL}/products\`); // Create services with their dependencies const userService = new UserService(userRepository); const productService = new ProductService(productRepository); // Services type interface Services { userService: UserService; productService: ProductService; } // Default services const defaultServices: Services = { userService, productService }; // Create context const ServiceContext = createContext<Services>(defaultServices); // Provider for dependency injection export const ServiceProvider: React.FC<{ services?: Partial<Services>; children: React.ReactNode; }> = ({ services, children }) => { const value = { ...defaultServices, ...services }; return ( <ServiceContext.Provider value={value}> {children} </ServiceContext.Provider> ); }; // Hook to use services export const useServices = () => useContext(ServiceContext); \`\`\` ### 6. Domain Hook with React Query Integration \`\`\`typescript // src/domain/user/hooks/useUser.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { User } from '../entities/User'; import { useServices } from '../../../context/ServiceContext'; // Query keys for React Query export const USER_QUERY_KEYS = { all: ['users'] as const, lists: () => [...USER_QUERY_KEYS.all, 'list'] as const, detail: (id: string) => [...USER_QUERY_KEYS.all, 'detail', id] as const, }; export function useUser(userId: string) { const { userService } = useServices(); const queryClient = useQueryClient(); // Query to get user const userQuery = useQuery({ queryKey: USER_QUERY_KEYS.detail(userId), queryFn: () => userService.getUser(userId), enabled: !!userId, }); // Mutation to update user const updateUserMutation = useMutation({ mutationFn: (updatedUser: User) => userService.updateUser(updatedUser), onSuccess: (updatedUser) => { queryClient.setQueryData(USER_QUERY_KEYS.detail(userId), updatedUser); queryClient.invalidateQueries({ queryKey: USER_QUERY_KEYS.lists() }); }, }); // Mutation to promote user to admin const promoteToAdminMutation = useMutation({ mutationFn: (id: string) => userService.promoteToAdmin(id), onSuccess: (updatedUser) => { queryClient.setQueryData(USER_QUERY_KEYS.detail(userId), updatedUser); queryClient.invalidateQueries({ queryKey: USER_QUERY_KEYS.lists() }); }, }); return { user: userQuery.data, isLoading: userQuery.isLoading, error: userQuery.error, updateUser: updateUserMutation.mutate, isUpdating: updateUserMutation.isPending, promoteToAdmin: promoteToAdminMutation.mutate, isPromoting: promoteToAdminMutation.isPending, }; } \`\`\` ### 7. React Component Using Domain Hook \`\`\`typescript // src/pages/UserProfilePage.tsx import React from 'react'; import { useParams } from 'react-router-dom'; import { useUser } from '../domain/user/hooks/useUser'; export const UserProfilePage: React.FC = () => { const { userId } = useParams<{ userId: string }>(); const { user, isLoading, error, updateUser, isUpdating, promoteToAdmin, isPromoting } = useUser(userId || ''); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; if (!user) return <div>User not found</div>; const handleNameChange = () => { const newName = prompt('New name:', user.name); if (newName && newName !== user.name) { updateUser({ ...user, name: newName }); } }; const handlePromoteToAdmin = () => { if (window.confirm('Promote this user to administrator?')) { promoteToAdmin(user.id); } }; return ( <div className="user-profile"> <h1>User Profile</h1> <div className="user-details"> <h2>{user.name}</h2> <p>Email: {user.email}</p> <p>Role: {user.role}</p> <p>Created: {new Date(user.createdAt).toLocaleDateString()}</p> </div> <div className="user-actions"> <button onClick={handleNameChange} disabled={isUpdating}> {isUpdating ? 'Updating...' : 'Change Name'} </button> {user.role !== 'admin' && ( <button onClick={handlePromoteToAdmin} disabled={isPromoting}> {isPromoting ? 'Promoting...' : 'Promote to Admin'} </button> )} </div> </div> ); }; \`\`\` ## Complete MSW Configuration ### 8. Domain-Specific MSW Organization #### Domain Mock Data \`\`\`typescript // src/domain/user/mocks/mockData.ts import { User } from '../entities/User'; export const mockUsers: Record<string, User> = { '1': { id: '1', name: 'John Doe', email: 'john@example.com', role: 'user', createdAt: '2023-01-01T00:00:00Z' }, '2': { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'admin', createdAt: '2023-01-02T00:00:00Z' } }; export function createMockUser(overrides?: Partial<User>): User { return { id: crypto.randomUUID(), name: 'Test User', email: 'test@example.com', role: 'user', createdAt: new Date().toISOString(), ...overrides }; } \`\`\` #### Domain MSW Handlers \`\`\`typescript // src/domain/user/mocks/handlers.ts import { http, HttpResponse } from 'msw'; import { mockUsers } from './mockData'; export const userHandlers = [ // GET /api/users http.get('/api/users', () => { return HttpResponse.json(Object.values(mockUsers)); }), // GET /api/users/:id http.get('/api/users/:id', ({ params }) => { const { id } = params; if (!mockUsers[id as string]) { return new HttpResponse(null, { status: 404 }); } return HttpResponse.json(mockUsers[id as string]); }), // PUT /api/users/:id http.put('/api/users/:id', async ({ params, request }) => { const { id } = params; if (!mockUsers[id as string]) { return new HttpResponse(null, { status: 404 }); } const updateData = await request.json(); mockUsers[id as string] = { ...mockUsers[id as string], ...updateData }; return HttpResponse.json(mockUsers[id as string]); }) ]; \`\`\` ### 9. Global MSW Configuration \`\`\`typescript // src/mocks/server.ts import { setupServer } from 'msw/node'; import { userHandlers } from '../domain/user/mocks/handlers'; import { productHandlers } from '../domain/product/mocks/handlers'; // Create MSW server with handlers from all domains export const server = setupServer( ...userHandlers, ...productHandlers ); \`\`\` ### 10. MSW Browser Worker \`\`\`typescript // src/mocks/browser.ts import { setupWorker } from 'msw/browser'; import { userHandlers } from '../domain/user/mocks/handlers'; import { productHandlers } from '../domain/product/mocks/handlers'; // Create MSW worker with handlers from all domains export const worker = setupWorker( ...userHandlers, ...productHandlers ); \`\`\` ### 11. Global Test Setup \`\`\`typescript // src/setupTests.ts import '@testing-library/jest-dom'; import { server } from './mocks/server'; // Enable MSW server before all tests beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); // Reset handlers between tests afterEach(() => server.resetHandlers()); // Close server after all tests afterAll(() => server.close()); \`\`\` ### 12. Development MSW Integration \`\`\`typescript // src/index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; async function bootstrap() { // Enable MSW in development only if (process.env.NODE_ENV === 'development') { const { worker } = await import('./mocks/browser'); await worker.start({ onUnhandledRequest: 'bypass' }); } const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( <React.StrictMode> <App /> </React.StrictMode> ); } bootstrap(); \`\`\` ## Complete Testing Examples ### 13. Service Test with Repository Mock \`\`\`typescript // src/domain/user/services/UserService.test.ts import { UserService } from './UserService'; import { UserPorts } from '../ports/UserPorts'; import { User } from '../entities/User'; const mockUser: User = { id: '1', name: 'Test User', email: 'test@example.com', role: 'user', createdAt: '2023-01-01T00:00:00Z' }; const mockUserRepository: UserPorts = { getUser: jest.fn().mockResolvedValue(mockUser), updateUser: jest.fn().mockImplementation((user) => Promise.resolve(user)), getUserList: jest.fn().mockResolvedValue([mockUser]), }; describe('UserService', () => { let userService: UserService; beforeEach(() => { jest.clearAllMocks(); userService = new UserService(mockUserRepository); }); it('should promote a user to admin', async () => { await userService.promoteToAdmin('1'); expect(mockUserRepository.updateUser).toHaveBeenCalledWith({ ...mockUser, role: 'admin' }); }); it('should throw error when promoting an admin', async () => { const adminUser = { ...mockUser, role: 'admin' }; mockUserRepository.getUser = jest.fn().mockResolvedValue(adminUser); await expect(userService.promoteToAdmin('1')).rejects.toThrow('User is already an admin'); }); }); \`\`\` ### 14. Adapter Test with Domain Mocks \`\`\`typescript // src/domain/user/adapters/UserApiAdapter.test.ts import { UserApiAdapter } from './UserApiAdapter'; import { server } from '../../../mocks/server'; import { http, HttpResponse } from 'msw'; import { createMockUser } from '../mocks/mockData'; describe('UserApiAdapter', () => { const adapter = new UserApiAdapter('/api/users'); beforeEach(() => { server.resetHandlers(); }); it('should fetch a user by id', async () => { server.use( http.get('/api/users/1', () => { return HttpResponse.json({ id: '1', name: 'John Doe', email: 'john@example.com', role: 'user', createdAt: '2023-01-01T00:00:00Z' }); }) ); const user = await adapter.getUser('1'); expect(user.id).toBe('1'); expect(user.name).toBe('John Doe'); }); }); \`\`\` ### 15. Hook Test with React Testing Library \`\`\`typescript // src/domain/user/hooks/useUser.test.ts import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useUser } from './useUser'; import { ServiceProvider } from '../../../context/ServiceContext'; import { UserService } from '../services/UserService'; // Test wrapper utility const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); return ({ children }) => ( <QueryClientProvider client={queryClient}> <ServiceProvider services={{ userService: mockUserService }}> {children} </ServiceProvider> </QueryClientProvider> ); }; describe('useUser hook', () => { it('should fetch user data', async () => { const { result } = renderHook(() => useUser('1'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.user).toBeDefined(); }); }); \`\`\` ## Best Practices ### Naming Conventions - **PascalCase**: Components, Classes, Interfaces (\`UserService\`, \`UserCard\`) - **camelCase**: Functions, Variables (\`getUserById\`, \`currentUser\`) - **kebab-case**: File names (\`user-service.ts\`, \`user-card.tsx\`) ### File Organization - Always keep tests co-located with their source files - One file per entity/service/adapter - Group exports in index.ts files - Never create separate test directories ### React Query Query Keys - Use constants for query keys (\`USER_QUERY_KEYS\`) - Consistent structure: \`[domain, type, ...params]\` - Always invalidate related queries on mutations ### Error Handling - Always handle errors in adapters - Provide meaningful error messages - Use React Query error boundaries ## What NOT to Do (Anti-Patterns) ### ❌ NEVER: Business Logic Outside Domains \`\`\`typescript // ❌ DON'T: Business logic in components const UserProfile = ({ userId }) => { const [user, setUser] = useState(); // ❌ Business logic in component const promoteToAdmin = () => { if (user.role === 'admin') { alert('Already admin'); return; } // ❌ Direct API call in component fetch(\`/api/users/\${userId}/promote\`, { method: 'POST' }); }; }; // ❌ DON'T: Business logic in utils // src/utils/userUtils.ts export function calculateUserPermissions(user) { // ❌ Business logic outside domain } \`\`\` ### ❌ NEVER: Manual Domain Creation \`\`\`bash # ❌ DON'T: Create folders manually mkdir src/domain/order mkdir src/domain/order/services # This bypasses Ruch conventions and tooling # ✅ DO: Use Ruch CLI ruch create order \`\`\` ### ❌ NEVER: Import Services Directly \`\`\`typescript // ❌ DON'T: Import services in components import { UserService } from '../domain/user/services/UserService'; const UserProfile = () => { const userService = new UserService(); // ❌ Direct instantiation // ... }; // ✅ DO: Use domain hooks import { useUser } from '../domain/user/hooks/useUser'; const UserProfile = () => { const { user, updateUser } = useUser(userId); // ✅ Through hook // ... }; \`\`\` ### ❌ NEVER: Cross-Domain Direct Imports \`\`\`typescript // ❌ DON'T: Import between domains directly import { OrderService } from '../order/services/OrderService'; export class UserService { // ❌ Direct dependency constructor(private orderService: OrderService) {} } // ✅ DO: Use ports for cross-domain communication export interface OrderPorts { getOrdersByUser(userId: string): Promise<Order[]>; } export class UserService { // ✅ Through interface constructor(private orderPorts: OrderPorts) {} } \`\`\` ## ✅ Best Practices Summary 1. **Always start with**: \`ruch create <domain>\` 2. **Business logic belongs in**: Domain services only 3. **React integration via**: Domain hooks only 4. **Cross-domain communication**: Through ports/adapters 5. **Testing strategy**: Co-located with MSW 6. **State management**: React Query in hooks 7. **Dependency injection**: ServiceContext only This comprehensive guide covers all aspects of the Ruch hexagonal architecture and must be followed to maintain consistency and code quality. `; const guidePath = path.join(process.cwd(), 'GUIDE.md'); await fs.writeFile(guidePath, guideContent); } /** * Generates domain-specific guide files */ async function generateDomainGuides(domains: Record<string, DomainStructure>): Promise<void> { for (const [domainName, domainStructure] of Object.entries(domains)) { const domainGuideContent = `# ${domainName.charAt(0).toUpperCase() + domainName.slice(1)} Domain Guide ## Overview The **${domainName}** domain manages all business logic related to ${domainName.toLowerCase()}. ## Domain Structure ### Components - **Entities**: Data models for ${domainName} - **Ports**: Interfaces for data access - **Services**: Domain business logic - **Adapters**: Concrete implementations (API, LocalStorage, etc.) - **Hooks**: Domain-specific React hooks - **UI**: React components for user interface ### Paths - Entities: \`${domainStructure.entities}\` - Ports: \`${domainStructure.ports}\` - Services: \`${domainStructure.services}\` - Adapters: \`${domainStructure.adapters}\` - Hooks: \`${domainStructure.hooks}\` - UI: \`${domainStructure.ui}\` ## Domain-Specific Examples ### ${domainName.charAt(0).toUpperCase() + domainName.slice(1)} Entity \`\`\`typescript export interface ${domainName.charAt(0).toUpperCase() + domainName.slice(1)} { id: string; // Add domain-specific properties here createdAt: string; updatedAt: string; } \`\`\` ### ${domainName.charAt(0).toUpperCase() + domainName.slice(1)}Repository Port \`\`\`typescript export interface ${domainName.charAt(0).toUpperCase() + domainName.slice(1)}Repository { get${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(id: string): Promise<${domainName.charAt(0).toUpperCase() + domainName.slice(1)}>; get${domainName.charAt(0).toUpperCase() + domainName.slice(1)}List(): Promise<${domainName.charAt(0).toUpperCase() + domainName.slice(1)}[]>; create${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(data: Omit<${domainName.charAt(0).toUpperCase() + domainName.slice(1)}, 'id'>): Promise<${domainName.charAt(0).toUpperCase() + domainName.slice(1)}>; update${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(${domainName.toLowerCase()}: ${domainName.charAt(0).toUpperCase() + domainName.slice(1)}): Promise<${domainName.charAt(0).toUpperCase() + domainName.slice(1)}>; delete${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(id: string): Promise<void>; } \`\`\` ### ${domainName.charAt(0).toUpperCase() + domainName.slice(1)} Service \`\`\`typescript export class ${domainName.charAt(0).toUpperCase() + domainName.slice(1)}Service { constructor(private ${domainName.toLowerCase()}Repository: ${domainName.charAt(0).toUpperCase() + domainName.slice(1)}Repository) {} async get${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(id: string): Promise<${domainName.charAt(0).toUpperCase() + domainName.slice(1)}> { return this.${domainName.toLowerCase()}Repository.get${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(id); } // Add domain-specific business logic here } \`\`\` ### use${domainName.charAt(0).toUpperCase() + domainName.slice(1)} Hook \`\`\`typescript export function use${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(${domainName.toLowerCase()}Id: string) { const { ${domainName.toLowerCase()}Service } = useServices(); const ${domainName.toLowerCase()}Query = useQuery({ queryKey: ['${domainName.toLowerCase()}', 'detail', ${domainName.toLowerCase()}Id], queryFn: () => ${domainName.toLowerCase()}Service.get${domainName.charAt(0).toUpperCase() + domainName.slice(1)}(${domainName.toLowerCase()}Id), enabled: !!${domainName.toLowerCase()}Id, }); return { ${domainName.toLowerCase()}: ${domainName.toLowerCase()}Query.data, isLoading: ${domainName.toLowerCase()}Query.isLoading, error: ${domainName.toLowerCase()}Query.error, }; } \`\`\` ## Specific Conventions - Use the \`${domainName.charAt(0).toUpperCase() + domainName.slice(1)}\` prefix for all domain entities - React Query keys should start with \`['${domainName.toLowerCase()}']\` - Place tests next to each file with \`.test.ts\` or \`.test.tsx\` extension ## Recommended Tests 1. **Services**: Test business logic with repository mocks 2. **Adapters**: Use MSW to mock API calls 3. **Hooks**: Test with React Testing Library and mocked providers 4. **Components**: Test rendering and user interactions ## MSW Integration for ${domainName.charAt(0).toUpperCase() + domainName.slice(1)} Domain ### MSW Commands for this Domain \`\`\`bash # Initialize MSW (if not done already) ruch msw init # Generate handlers and mocks for all domains ruch msw handlers generate ruch msw mocks generate # Update this specific domain only ruch msw update ${domainName} \`\`\` ### ⚠️ CRITICAL: Customize Generated MSW Files The MSW commands generate **template files only**. You MUST customize them: 1. **Handler Customization** (\`src/mocks/handlers/${domainName}.ts\`): - Update endpoints to match your real API routes - Add authentication/authorization logic - Implement proper validation - Add realistic error scenarios 2. **Mock Data Customization** (\`src/domains/${domainName}/mocks/mockData.ts\`): - Replace placeholder data ("Sample firstName 1") with realistic business data - Add domain-specific validation rules - Implement proper business logic - Create realistic test scenarios `; const domainGuidePath = path.join(process.cwd(), 'src', 'domains', domainName, 'GUIDE.md'); await fs.ensureDir(path.dirname(domainGuidePath)); await fs.writeFile(domainGuidePath, domainGuideContent); } } /** * Generates tool-specific configuration files */ async function generateToolConfigurations(tools: string[], domains: Record<string, DomainStructure>): Promise<void> { for (const tool of tools) { switch (tool.toLowerCase()) { case 'cursor': await generateCursorConfig(domains); break; case 'copilot': await generateCopilotConfig(domains); break; case 'windsurf': await generateWindsurfConfig(domains); break; case 'juni': await generateJuniConfig(domains); break; default: console.warn(chalk.yellow(`Warning: Unknown tool "${tool}". Supported tools: cursor, copilot, windsurf, juni`)); } } } /** * Generates Cursor-specific configuration (.cursor/rules/ruch-architecture.mdc) */ async function generateCursorConfig(domains: Record<string, DomainStructure>): Promise<void> { const domainList = Object.keys(domains).join(', '); const cursorRulesMdc = `--- description: Ruch Hexagonal Architecture Rules for Cursor - Enforces business logic isolation and proper domain structure globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] alwaysApply: true --- # Ruch Hexagonal Architecture Rules for Cursor ## Project Architecture This project follows a Simplified Hexagonal Architecture for React with the following structure: - **Domains**: ${domainList} - **Structure**: Each domain contains entities/, ports/, services/, adapters/, hooks/, ui/ ## CRITICAL Rules ### 1. Business Logic Isolation - ALL business logic MUST be in domains - NEVER in components, pages, utils, or shared hooks - Components handle ONLY UI logic and user interactions - Business rules, validations, calculations belong in domain services ### 2. Always Use Ruch CLI - Create domains: \`ruch create <domain-name>\` - List domains: \`ruch list\` - Update context: \`ruch context generate\` - NEVER create domain folders manually ### 3. No Direct Service Imports - NEVER import services directly in components - ALWAYS use domain hooks that wrap services with React Query - Components → Hooks → Services → Adapters ### 4. Testing Strategy - Co-locate tests with .test.ts/.test.tsx extensions - Use MSW for API mocking in tests - Test services with mocked repositories - Store domain-specific mocks in domain mocks/ folders for better organization - NEVER mock functions or hooks - use MSW to mock APIs instead - Minimize mock usage - prefer real implementations with MSW for API responses ### 5. Cross-Domain Communication - Use ports/adapters for domain communication - NEVER import services/entities directly between domains ### 6. Mock Data Organization - Place domain-specific mocks in {domain}/mocks/ folders - Use global mocks/ folder only for shared/cross-cutting concerns - Keep MSW handlers and mock data close to domain logic ### 7. Hook and Function Usage Rules - NEVER create local mock implementations of domain hooks in components - ALWAYS import and use actual domain hooks from their designated locations - NEVER create barrel export files (index.ts) with mock implementations - If you need test data, use MSW handlers, not inline mocks in hooks ## File Structure Requirements \`\`\` src/ ├── domain/ # All business domains │ └── {domain}/ │ ├── entities/ # Business models │ ├── ports/ # Interfaces │ ├── services/ # Business logic │ ├── adapters/ # API implementations │ ├── hooks/ # React Query hooks │ ├── ui/ # Domain components │ └── mocks/ # Domain-specific MSW handlers & mock data ├── components/ # Shared UI components ├── pages/ # Route components ├── context/ # ServiceProvider for DI └── mocks/ # Global MSW configuration (server.ts, browser.ts) \`\`\` ## React Query Patterns - Use consistent query keys: \`[domain, type, ...identifiers]\` - Define query key constants: \`DOMAIN_QUERY_KEYS\` - Handle loading, error, success states - Implement optimistic updates ## TypeScript Requirements - Use TypeScript for all files - Add JSDoc to all public methods - Define strict types for entities - Use discriminated unions for entity states ## Code Examples ### ✅ Correct Component Pattern \`\`\`typescript // Component uses hooks, not services directly const UserProfile = () => { const { user, updateUser, isLoading } = useUser(userId); // Only UI logic here }; \`\`\` ### ✅ Correct Hook Pattern \`\`\`typescript export function useUser(userId: string) { const { userService } = useServices(); return useQuery({ queryKey: ['user', 'detail', userId], queryFn: () => userService.getUser(userId) }); } \`\`\` ### ❌ Anti-Pattern: Direct Service Import \`\`\`typescript // NEVER do this in components import { UserService } from '../domain/user/services'; \`\`\` ### ❌ Anti-Pattern: Local Hook Mocks \`\`\`typescript // NEVER create local mock hooks in components function useProduct(id: string) { return { data: mockProduct, isLoading: false }; } function ProductDetail() { const { data } = useProduct(id); // ❌ Using local mock } \`\`\` ### ✅ Correct: Import Actual Domain Hooks \`\`\`typescript // ALWAYS import real hooks from domains import { useProduct } from '../domains/product/hooks'; function ProductDetail() { const { data, isLoading } = useProduct(id); // ✅ Using real hook } \`\`\` ### ❌ Anti-Pattern: Function Mocking \`\`\`typescript // NEVER mock functions directly const mockUserService = { getUser: jest.fn().mockResolvedValue(mockUser) }; \`\`\` ### ✅ Correct: MSW API Mocking \`\`\`typescript // ALWAYS use MSW to mock API responses server.use( http.get('/api/users/:id', () => { return HttpResponse.json(mockUser); }) ); \`\`\` Always prioritize domain isolation and use the Ruch CLI for domain management. `; // Create .cursor/rules directory if it doesn't exist await fs.ensureDir(path.join(process.cwd(), '.cursor', 'rules')); // Write the new .mdc rule file const cursorRulesPath = path.join(process.cwd(), '.cursor', 'rules', 'ruch-architecture.mdc'); await fs.writeFile(cursorRulesPath, cursorRulesMdc); // Remove legacy .cursorrules file if it exists const legacyPath = path.join(process.cwd(), '.cursorrules'); try { await fs.remove(legacyPath); } catch (error) { // File doesn't exist, ignore } } /** * Generates GitHub Copilot configuration (.github/copilot-instructions.md) * Updated for 2025 - using repository custom instructions format */ async function generateCopilotConfig(domains: Record<string, DomainStructure>): Promise<void> { const domainList = Object.keys(domains).join(', '); const copilotInstructions = `# GitHub Copilot Repository Instructions for Ruch Architecture ## Project Context This React TypeScript project uses Simplified Hexagonal Architecture. **Business Domains**: ${domainList} ## Development Rules ### Domain-Driven Architecture When adding business functionality, always use: \`ruch create <domain>\` Keep ALL business logic in domain services, never in React components or utilities. ### File Organization - Use domain structure: entities/, ports/, services/, adapters/, hooks/, ui/ - Co-locate tests with .test.ts/.test.tsx extensions - Use React Query in hooks for data management ### Import Patterns Components import domain hooks, not services directly. Services use ports for data access. Adapters implement port interfaces. ### Code Generation Preferences Generate TypeScript with JSDoc comments. Use PascalCase for components, camelCase for functions. Always include proper error handling and loading states. ### Testing Approach Use MSW for API mocking in tests. Write tests co-located with source files. Test business logic in services with mocked ports. ## Anti-Patterns to Avoid Never put business logic in components. Never import services directly in components. Never manually create domain folders. Never import services/entities directly between domains. Always suggest Ruch CLI commands for domain management and follow hexagonal architecture principles. `; await fs.ensureDir(path.join(process.cwd(), '.github')); const copilotPath = path.join(process.cwd(), '.github', 'copilot-instructions.md'); await fs.writeFile(copilotPath, copilotInstructions); } /** * Generates Windsurf configuration (.windsurfrules) * Updated for 2025 - keeping current format as it's still supported */ async function generateWindsurfConfig(domains: Record<string, DomainStructure>): Promise<void> { const domainList = Object.keys(domains).join(', '); const windsurfRules = `# Windsurf AI Rules for Ruch Hexagonal Architecture ## Project Overview React TypeScript project with Simplified Hexagonal Architecture **Domains**: ${domainList} ## Code Generation Guidelines ### Domain Management - Use Ruch CLI: \`ruch create <domain>\` for new business capabilities - Structure: entities/ → ports/ → services/ → adapters/ → hooks/ → ui/ - Update context: \`ruch context generate\` after domain creation ### Architecture Principles 1. **Business Logic Isolation**: Only in domain services, never in c