665 lines
14 KiB
Markdown
665 lines
14 KiB
Markdown
# 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
|