feat: add implementation plan and monorepo guide for DreamChat project
This commit is contained in:
664
doc/monorepo-guide.md
Normal file
664
doc/monorepo-guide.md
Normal file
@@ -0,0 +1,664 @@
|
||||
# DreamChat Monorepo Guide
|
||||
|
||||
## Overview
|
||||
|
||||
DreamChat uses a pnpm workspace monorepo structure to share code between frontend and backend, particularly for WebSocket types and API DTOs.
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
dreamchat/
|
||||
├── apps/
|
||||
│ ├── backend/ # NestJS application
|
||||
│ └── frontend/ # React + Vite application
|
||||
├── packages/
|
||||
│ ├── shared/ # Shared types & interfaces
|
||||
│ └── config/ # Shared configurations (ESLint, TS)
|
||||
├── prisma/ # Database schema (shared)
|
||||
├── pnpm-workspace.yaml
|
||||
└── package.json # Root package.json
|
||||
```
|
||||
|
||||
## Root Configuration
|
||||
|
||||
### package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dreamchat",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8.15.0",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"dev": "pnpm -r --parallel dev",
|
||||
"test": "pnpm -r test",
|
||||
"lint": "pnpm -r lint",
|
||||
"db:migrate": "pnpm --filter backend db:migrate",
|
||||
"db:generate": "prisma generate",
|
||||
"db:studio": "prisma studio",
|
||||
"clean": "pnpm -r clean && rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### pnpm-workspace.yaml
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
```
|
||||
|
||||
### .npmrc
|
||||
|
||||
```
|
||||
shamefully-hoist=true
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
```
|
||||
|
||||
## Package Structure
|
||||
|
||||
### 1. Shared Package (`packages/shared`)
|
||||
|
||||
Contains all shared types, interfaces, and DTOs used by both frontend and backend.
|
||||
|
||||
#### packages/shared/package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@dreamchat/shared",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./websocket": {
|
||||
"import": "./dist/websocket/index.js",
|
||||
"types": "./dist/websocket/index.d.ts"
|
||||
},
|
||||
"./api": {
|
||||
"import": "./dist/api/index.js",
|
||||
"types": "./dist/api/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### packages/shared/tsconfig.json
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
#### packages/shared/src/websocket/events.ts
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* WebSocket Event Types
|
||||
* Shared between frontend and backend for type-safe communication
|
||||
*/
|
||||
|
||||
export enum WebSocketEventType {
|
||||
// Client -> Server
|
||||
JOIN_CONVERSATION = 'JOIN_CONVERSATION',
|
||||
LEAVE_CONVERSATION = 'LEAVE_CONVERSATION',
|
||||
SEND_MESSAGE = 'SEND_MESSAGE',
|
||||
STOP_GENERATION = 'STOP_GENERATION',
|
||||
|
||||
// Server -> Client
|
||||
CONVERSATION_JOINED = 'CONVERSATION_JOINED',
|
||||
MESSAGE_ACK = 'MESSAGE_ACK',
|
||||
STREAM_CHUNK = 'STREAM_CHUNK',
|
||||
STREAM_COMPLETE = 'STREAM_COMPLETE',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
// Base message interface
|
||||
export interface WebSocketMessage<T = unknown> {
|
||||
type: WebSocketEventType;
|
||||
payload: T;
|
||||
timestamp: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
// Client -> Server payloads
|
||||
export interface JoinConversationPayload {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
export interface LeaveConversationPayload {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
export interface SendMessagePayload {
|
||||
conversationId: string;
|
||||
content: string;
|
||||
streaming?: boolean;
|
||||
}
|
||||
|
||||
export interface StopGenerationPayload {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
// Server -> Client payloads
|
||||
export interface ConversationJoinedPayload {
|
||||
conversationId: string;
|
||||
history: MessageHistoryItem[];
|
||||
}
|
||||
|
||||
export interface MessageHistoryItem {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface MessageAckPayload {
|
||||
messageId: string;
|
||||
status: 'received' | 'processing' | 'error';
|
||||
}
|
||||
|
||||
export interface StreamChunkPayload {
|
||||
conversationId: string;
|
||||
chunk: string;
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
export interface StreamCompletePayload {
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
content: string;
|
||||
tokensUsed?: number;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface ErrorPayload {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
#### packages/shared/src/websocket/index.ts
|
||||
|
||||
```typescript
|
||||
export * from './events.js';
|
||||
```
|
||||
|
||||
#### packages/shared/src/api/dto.ts
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Shared API DTOs
|
||||
* Used by backend for validation and frontend for type safety
|
||||
*/
|
||||
|
||||
// Character DTOs
|
||||
export interface CreateCharacterDto {
|
||||
name: string;
|
||||
personalityPrompt: string;
|
||||
backstory?: string;
|
||||
attributes?: Record<string, unknown>;
|
||||
avatarUrl?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UpdateCharacterDto extends Partial<CreateCharacterDto> {}
|
||||
|
||||
export interface CharacterResponseDto {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
personalityPrompt: string;
|
||||
backstory?: string;
|
||||
attributes: Record<string, unknown>;
|
||||
config: Record<string, unknown>;
|
||||
isPublic: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Conversation DTOs
|
||||
export interface CreateConversationDto {
|
||||
characterId: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ConversationResponseDto {
|
||||
id: string;
|
||||
title?: string;
|
||||
characterId: string;
|
||||
messageCount: number;
|
||||
totalTokens: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Message DTOs
|
||||
export interface CreateMessageDto {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface MessageResponseDto {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
tokensUsed?: number;
|
||||
model?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// Auth DTOs
|
||||
export interface LoginDto {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterDto {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthResponseDto {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
user: UserResponseDto;
|
||||
}
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: 'USER' | 'ADMIN';
|
||||
}
|
||||
```
|
||||
|
||||
#### packages/shared/src/api/index.ts
|
||||
|
||||
```typescript
|
||||
export * from './dto.js';
|
||||
```
|
||||
|
||||
#### packages/shared/src/index.ts
|
||||
|
||||
```typescript
|
||||
export * from './websocket/index.js';
|
||||
export * from './api/index.js';
|
||||
```
|
||||
|
||||
### 2. Backend (`apps/backend`)
|
||||
|
||||
#### apps/backend/package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@dreamchat/backend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"start": "node dist/main",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||
"db:migrate": "prisma migrate deploy",
|
||||
"db:generate": "prisma generate",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dreamchat/shared": "workspace:*",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.0.0",
|
||||
"@prisma/client": "^5.10.0",
|
||||
"@xenova/transformers": "^2.15.0",
|
||||
"socket.io": "^4.7.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"prisma": "^5.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Shared Package in Backend
|
||||
|
||||
```typescript
|
||||
// apps/backend/src/chat/chat.gateway.ts
|
||||
import {
|
||||
WebSocketEventType,
|
||||
WebSocketMessage,
|
||||
JoinConversationPayload,
|
||||
SendMessagePayload,
|
||||
StreamChunkPayload,
|
||||
ConversationJoinedPayload,
|
||||
} from '@dreamchat/shared';
|
||||
|
||||
@WebSocketGateway({ namespace: 'chat' })
|
||||
export class ChatGateway implements OnGatewayConnection {
|
||||
|
||||
@SubscribeMessage(WebSocketEventType.JOIN_CONVERSATION)
|
||||
async handleJoin(
|
||||
@MessageBody() payload: JoinConversationPayload,
|
||||
@ConnectedSocket() client: Socket,
|
||||
) {
|
||||
// Implementation
|
||||
const response: WebSocketMessage<ConversationJoinedPayload> = {
|
||||
type: WebSocketEventType.CONVERSATION_JOINED,
|
||||
payload: { conversationId: payload.conversationId, history: [] },
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
client.emit(response.type, response);
|
||||
}
|
||||
|
||||
@SubscribeMessage(WebSocketEventType.SEND_MESSAGE)
|
||||
async handleMessage(
|
||||
@MessageBody() payload: SendMessagePayload,
|
||||
@ConnectedSocket() client: Socket,
|
||||
) {
|
||||
// Stream chunks with type safety
|
||||
const chunk: WebSocketMessage<StreamChunkPayload> = {
|
||||
type: WebSocketEventType.STREAM_CHUNK,
|
||||
payload: {
|
||||
conversationId: payload.conversationId,
|
||||
chunk: ' partial text',
|
||||
isComplete: false,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
client.emit(chunk.type, chunk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Frontend (`apps/frontend`)
|
||||
|
||||
#### apps/frontend/package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@dreamchat/frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"lint": "eslint . --ext ts,tsx",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dreamchat/shared": "workspace:*",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"socket.io-client": "^4.7.0",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vitest": "^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Shared Package in Frontend
|
||||
|
||||
```typescript
|
||||
// apps/frontend/src/hooks/use-chat.ts
|
||||
import { useCallback } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import {
|
||||
WebSocketEventType,
|
||||
WebSocketMessage,
|
||||
JoinConversationPayload,
|
||||
SendMessagePayload,
|
||||
StreamChunkPayload,
|
||||
StreamCompletePayload,
|
||||
} from '@dreamchat/shared';
|
||||
|
||||
export function useChat(conversationId: string) {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [messages, setMessages] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const s = io('ws://localhost:3000/chat', {
|
||||
auth: { token: getAuthToken() },
|
||||
});
|
||||
|
||||
s.on(WebSocketEventType.STREAM_CHUNK, (msg: WebSocketMessage<StreamChunkPayload>) => {
|
||||
setMessages((prev) => [...prev, msg.payload.chunk]);
|
||||
});
|
||||
|
||||
s.on(WebSocketEventType.STREAM_COMPLETE, (msg: WebSocketMessage<StreamCompletePayload>) => {
|
||||
// Handle completion
|
||||
});
|
||||
|
||||
setSocket(s);
|
||||
|
||||
// Join conversation
|
||||
const joinPayload: JoinConversationPayload = { conversationId };
|
||||
s.emit(WebSocketEventType.JOIN_CONVERSATION, joinPayload);
|
||||
|
||||
return () => {
|
||||
s.disconnect();
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
const sendMessage = useCallback((content: string) => {
|
||||
if (!socket) return;
|
||||
|
||||
const payload: SendMessagePayload = {
|
||||
conversationId,
|
||||
content,
|
||||
streaming: true,
|
||||
};
|
||||
socket.emit(WebSocketEventType.SEND_MESSAGE, payload);
|
||||
}, [socket, conversationId]);
|
||||
|
||||
return { messages, sendMessage };
|
||||
}
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# 1. Install pnpm (if not already installed)
|
||||
npm install -g pnpm
|
||||
|
||||
# 2. Clone and setup
|
||||
git clone <repo-url>
|
||||
cd dreamchat
|
||||
|
||||
# 3. Install all dependencies
|
||||
pnpm install
|
||||
|
||||
# 4. Build shared packages first
|
||||
pnpm --filter @dreamchat/shared build
|
||||
|
||||
# 5. Generate Prisma client
|
||||
pnpm db:generate
|
||||
|
||||
# 6. Start development (runs all apps in parallel)
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Adding Dependencies
|
||||
|
||||
```bash
|
||||
# Add dependency to root
|
||||
pnpm add -D typescript
|
||||
|
||||
# Add dependency to specific app
|
||||
pnpm --filter @dreamchat/backend add @nestjs/jwt
|
||||
|
||||
# Add dependency to shared package
|
||||
pnpm --filter @dreamchat/shared add zod
|
||||
|
||||
# Add shared package to backend/frontend
|
||||
pnpm --filter @dreamchat/backend add @dreamchat/shared@workspace:*
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Build specific package
|
||||
pnpm --filter @dreamchat/shared build
|
||||
|
||||
# Watch mode for shared package (during development)
|
||||
pnpm --filter @dreamchat/shared dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Test all
|
||||
pnpm test
|
||||
|
||||
# Test specific package
|
||||
pnpm --filter @dreamchat/backend test
|
||||
|
||||
# Test with coverage
|
||||
pnpm --filter @dreamchat/backend test --coverage
|
||||
```
|
||||
|
||||
## Docker Integration
|
||||
|
||||
### Development Docker Compose
|
||||
|
||||
```yaml
|
||||
# docker-compose.dev.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/backend/Dockerfile
|
||||
target: development
|
||||
volumes:
|
||||
- .:/workspace
|
||||
- /workspace/node_modules
|
||||
- /workspace/apps/backend/node_modules
|
||||
- /workspace/packages/shared/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
command: pnpm --filter @dreamchat/backend dev
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/frontend/Dockerfile
|
||||
target: development
|
||||
volumes:
|
||||
- .:/workspace
|
||||
- /workspace/node_modules
|
||||
- /workspace/apps/frontend/node_modules
|
||||
- /workspace/packages/shared/node_modules
|
||||
command: pnpm --filter @dreamchat/frontend dev
|
||||
```
|
||||
|
||||
### Production Dockerfile (Backend)
|
||||
|
||||
```dockerfile
|
||||
# apps/backend/Dockerfile
|
||||
FROM node:20-alpine AS base
|
||||
RUN npm install -g pnpm
|
||||
|
||||
FROM base AS dependencies
|
||||
WORKDIR /app
|
||||
COPY pnpm-workspace.yaml package.json ./
|
||||
COPY apps/backend/package.json ./apps/backend/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY --from=dependencies /app/apps/backend/node_modules ./apps/backend/node_modules
|
||||
COPY --from=dependencies /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||
COPY . .
|
||||
RUN pnpm --filter @dreamchat/shared build
|
||||
RUN pnpm --filter @dreamchat/backend build
|
||||
|
||||
FROM base AS production
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/apps/backend/dist ./dist
|
||||
COPY --from=build /app/apps/backend/node_modules ./node_modules
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/main.js"]
|
||||
```
|
||||
|
||||
## Benefits of This Structure
|
||||
|
||||
1. **Type Safety**: Shared WebSocket events and DTOs ensure frontend and backend stay in sync
|
||||
2. **Single Source of Truth**: Changes to types in `packages/shared` propagate to both apps
|
||||
3. **Efficient Development**: pnpm workspaces deduplicate dependencies, saving disk space
|
||||
4. **Simplified CI/CD**: Single repository with unified versioning
|
||||
5. **Code Reuse**: Utilities and constants can be shared
|
||||
|
||||
## Migration from Non-Monorepo
|
||||
|
||||
If migrating an existing project:
|
||||
|
||||
1. Create new monorepo structure
|
||||
2. Move backend to `apps/backend`
|
||||
3. Move frontend to `apps/frontend`
|
||||
4. Extract shared types to `packages/shared`
|
||||
5. Update all imports to use `@dreamchat/shared`
|
||||
6. Update Docker files to use pnpm
|
||||
Reference in New Issue
Block a user