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
text/typescript
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