494 lines
13 KiB
Markdown
494 lines
13 KiB
Markdown
|
|
# Server TypeScript Conversion Plan
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
Convert the Node.js server from JavaScript to TypeScript to improve type safety, developer experience, and enable proper imports of TypeScript modules (database, schemas, etc.) from within the server code.
|
||
|
|
|
||
|
|
## Current State Analysis
|
||
|
|
|
||
|
|
### Files to Convert
|
||
|
|
1. **`server.js`** (57 lines) - Main server entry point
|
||
|
|
- Handles Vite dev server setup
|
||
|
|
- Production build serving
|
||
|
|
- Socket.IO initialization
|
||
|
|
- Express middleware
|
||
|
|
|
||
|
|
2. **`server/socket.js`** (89 lines) - Socket.IO server
|
||
|
|
- Connection handling
|
||
|
|
- Draft room management
|
||
|
|
- Global instance storage
|
||
|
|
|
||
|
|
### Existing TypeScript Configuration
|
||
|
|
- **`tsconfig.node.json`** already includes `server/**/*.ts`
|
||
|
|
- Module resolution is set to "bundler" with ES2022 target
|
||
|
|
- Path aliases configured: `~/` for app, `~/database/` for database
|
||
|
|
|
||
|
|
### Dependencies Already Installed
|
||
|
|
- TypeScript is already in devDependencies
|
||
|
|
- `@types/node` is configured in tsconfig.node.json
|
||
|
|
- Vite handles TypeScript compilation in development
|
||
|
|
|
||
|
|
## Implementation Steps
|
||
|
|
|
||
|
|
### Step 1: Install Required Dependencies
|
||
|
|
```bash
|
||
|
|
npm install --save-dev tsx @types/express @types/morgan @types/compression
|
||
|
|
```
|
||
|
|
|
||
|
|
**Why tsx?**
|
||
|
|
- Fastest TypeScript runner for Node.js
|
||
|
|
- No compilation step needed
|
||
|
|
- Works in both development and production
|
||
|
|
- Supports ESM modules and path aliases
|
||
|
|
- Better than ts-node for performance
|
||
|
|
|
||
|
|
### Step 2: Convert server.js to server.ts
|
||
|
|
|
||
|
|
**File: `server.ts`**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import compression from "compression";
|
||
|
|
import express, { Express } from "express";
|
||
|
|
import morgan from "morgan";
|
||
|
|
import { createServer } from "http";
|
||
|
|
import { ViteDevServer } from "vite";
|
||
|
|
import path from "path";
|
||
|
|
import { fileURLToPath } from "url";
|
||
|
|
|
||
|
|
// ESM module resolution helpers
|
||
|
|
const __filename = fileURLToPath(import.meta.url);
|
||
|
|
const __dirname = path.dirname(__filename);
|
||
|
|
|
||
|
|
// Configuration
|
||
|
|
const BUILD_PATH = "./build/server/index.js";
|
||
|
|
const DEVELOPMENT = process.env.NODE_ENV === "development";
|
||
|
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||
|
|
|
||
|
|
async function createAppServer(): Promise<void> {
|
||
|
|
const app: Express = express();
|
||
|
|
|
||
|
|
app.use(compression());
|
||
|
|
app.disable("x-powered-by");
|
||
|
|
|
||
|
|
if (DEVELOPMENT) {
|
||
|
|
console.log("Starting development server");
|
||
|
|
|
||
|
|
// Dynamic import to avoid bundling Vite in production
|
||
|
|
const { createServer: createViteServer } = await import("vite");
|
||
|
|
|
||
|
|
const viteDevServer: ViteDevServer = await createViteServer({
|
||
|
|
server: { middlewareMode: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
app.use(viteDevServer.middlewares);
|
||
|
|
|
||
|
|
app.use(async (req, res, next) => {
|
||
|
|
try {
|
||
|
|
const source = await viteDevServer.ssrLoadModule("./server/app.ts");
|
||
|
|
return await source.app(req, res, next);
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof Error) {
|
||
|
|
viteDevServer.ssrFixStacktrace(error);
|
||
|
|
}
|
||
|
|
next(error);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
console.log("Starting production server");
|
||
|
|
|
||
|
|
app.use(
|
||
|
|
"/assets",
|
||
|
|
express.static("build/client/assets", {
|
||
|
|
immutable: true,
|
||
|
|
maxAge: "1y"
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
app.use(morgan("tiny"));
|
||
|
|
app.use(express.static("build/client", { maxAge: "1h" }));
|
||
|
|
|
||
|
|
// Dynamic import for production build
|
||
|
|
const { app: productionApp } = await import(BUILD_PATH);
|
||
|
|
app.use(productionApp);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create HTTP server
|
||
|
|
const httpServer = createServer(app);
|
||
|
|
|
||
|
|
// Initialize Socket.IO
|
||
|
|
const { initializeSocketIO } = await import("./server/socket.js");
|
||
|
|
initializeSocketIO(httpServer);
|
||
|
|
|
||
|
|
// Start server
|
||
|
|
httpServer.listen(PORT, () => {
|
||
|
|
console.log(`Server is running on http://localhost:${PORT}`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Start the server
|
||
|
|
createAppServer().catch((error) => {
|
||
|
|
console.error("Failed to start server:", error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 3: Convert server/socket.js to server/socket.ts
|
||
|
|
|
||
|
|
**File: `server/socket.ts`**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Server as SocketIOServer, Socket } from "socket.io";
|
||
|
|
import { Server as HTTPServer } from "http";
|
||
|
|
|
||
|
|
// Global type augmentation for Socket.IO instance
|
||
|
|
declare global {
|
||
|
|
var __socketIO: SocketIOServer | undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
let io: SocketIOServer | null = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize Socket.IO server
|
||
|
|
*/
|
||
|
|
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||
|
|
if (io) {
|
||
|
|
console.log("Socket.IO already initialized");
|
||
|
|
return io;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create Socket.IO server with proper typing
|
||
|
|
io = new SocketIOServer(httpServer, {
|
||
|
|
cors: process.env.NODE_ENV === "production" && process.env.APP_URL
|
||
|
|
? {
|
||
|
|
origin: process.env.APP_URL,
|
||
|
|
credentials: true,
|
||
|
|
}
|
||
|
|
: undefined,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Connection handling with typed socket
|
||
|
|
io.on("connection", (socket: Socket) => {
|
||
|
|
console.log("Client connected:", socket.id);
|
||
|
|
|
||
|
|
// Join draft room
|
||
|
|
socket.on("join-draft", (seasonId: string) => {
|
||
|
|
if (!seasonId) {
|
||
|
|
console.error("No seasonId provided for join-draft");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
socket.join(`draft-${seasonId}`);
|
||
|
|
console.log(`Socket ${socket.id} joined draft-${seasonId}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Leave draft room
|
||
|
|
socket.on("leave-draft", (seasonId: string) => {
|
||
|
|
if (!seasonId) return;
|
||
|
|
socket.leave(`draft-${seasonId}`);
|
||
|
|
console.log(`Socket ${socket.id} left draft-${seasonId}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Test event handler
|
||
|
|
socket.on("test-event", (data: any) => {
|
||
|
|
console.log("📨 Received test-event from client:", socket.id, data);
|
||
|
|
|
||
|
|
socket.emit("test-message", {
|
||
|
|
originalMessage: data,
|
||
|
|
serverResponse: "Hello from server!",
|
||
|
|
serverTimestamp: new Date().toISOString(),
|
||
|
|
socketId: socket.id,
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log("✅ Sent test-message response to client:", socket.id);
|
||
|
|
});
|
||
|
|
|
||
|
|
socket.on("disconnect", () => {
|
||
|
|
console.log("Client disconnected:", socket.id);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Store globally for access in route handlers
|
||
|
|
global.__socketIO = io;
|
||
|
|
|
||
|
|
console.log("Socket.IO initialized");
|
||
|
|
return io;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the Socket.IO server instance
|
||
|
|
*/
|
||
|
|
export function getSocketIO(): SocketIOServer {
|
||
|
|
const instance = io || global.__socketIO;
|
||
|
|
if (!instance) {
|
||
|
|
throw new Error("Socket.IO not initialized. Call initializeSocketIO first.");
|
||
|
|
}
|
||
|
|
return instance;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 4: Create Type Definitions for Socket Events (Optional but Recommended)
|
||
|
|
|
||
|
|
**File: `server/socket.types.ts`**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Define your socket event types for better type safety
|
||
|
|
export interface ServerToClientEvents {
|
||
|
|
"test-message": (data: {
|
||
|
|
originalMessage: any;
|
||
|
|
serverResponse: string;
|
||
|
|
serverTimestamp: string;
|
||
|
|
socketId: string;
|
||
|
|
}) => void;
|
||
|
|
"pick-made": (data: any) => void;
|
||
|
|
"draft-started": (data: { seasonId: string; currentPickNumber: number }) => void;
|
||
|
|
"draft-completed": () => void;
|
||
|
|
"timer-update": (data: {
|
||
|
|
seasonId: string;
|
||
|
|
teamId: string;
|
||
|
|
timeRemaining: number;
|
||
|
|
currentPickNumber: number;
|
||
|
|
}) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ClientToServerEvents {
|
||
|
|
"join-draft": (seasonId: string) => void;
|
||
|
|
"leave-draft": (seasonId: string) => void;
|
||
|
|
"test-event": (data: any) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface InterServerEvents {
|
||
|
|
ping: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SocketData {
|
||
|
|
userId?: string;
|
||
|
|
teamId?: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Then update `server/socket.ts` to use these types:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { Server as SocketIOServer } from "socket.io";
|
||
|
|
import type {
|
||
|
|
ServerToClientEvents,
|
||
|
|
ClientToServerEvents,
|
||
|
|
InterServerEvents,
|
||
|
|
SocketData
|
||
|
|
} from "./socket.types";
|
||
|
|
|
||
|
|
// Create typed Socket.IO server
|
||
|
|
io = new SocketIOServer<
|
||
|
|
ClientToServerEvents,
|
||
|
|
ServerToClientEvents,
|
||
|
|
InterServerEvents,
|
||
|
|
SocketData
|
||
|
|
>(httpServer, { /* ... */ });
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 5: Update package.json Scripts
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"scripts": {
|
||
|
|
"build": "react-router build",
|
||
|
|
"db:generate": "dotenv -- drizzle-kit generate",
|
||
|
|
"db:migrate": "dotenv -- drizzle-kit migrate",
|
||
|
|
"dev": "dotenv -- tsx watch server.ts",
|
||
|
|
"start": "tsx server.ts",
|
||
|
|
"start:production": "drizzle-kit migrate && tsx server.ts",
|
||
|
|
"typecheck": "react-router typegen && tsc -b"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Key Changes:**
|
||
|
|
- `dev`: Uses `tsx watch` for auto-restart on file changes
|
||
|
|
- `start`: Uses `tsx` to run TypeScript directly
|
||
|
|
- `start:production`: Same as start but with migration
|
||
|
|
|
||
|
|
### Step 6: Update Dockerfile
|
||
|
|
|
||
|
|
```dockerfile
|
||
|
|
FROM node:20-alpine AS development-dependencies-env
|
||
|
|
COPY . /app
|
||
|
|
WORKDIR /app
|
||
|
|
RUN npm ci
|
||
|
|
|
||
|
|
FROM node:20-alpine AS production-dependencies-env
|
||
|
|
COPY ./package.json package-lock.json /app/
|
||
|
|
WORKDIR /app
|
||
|
|
RUN npm ci --omit=dev
|
||
|
|
|
||
|
|
FROM node:20-alpine AS build-env
|
||
|
|
COPY . /app/
|
||
|
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||
|
|
WORKDIR /app
|
||
|
|
RUN npm run build
|
||
|
|
|
||
|
|
FROM node:20-alpine
|
||
|
|
# Copy TypeScript files instead of JavaScript
|
||
|
|
COPY ./package.json package-lock.json server.ts /app/
|
||
|
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||
|
|
COPY --from=build-env /app/build /app/build
|
||
|
|
COPY ./drizzle /app/drizzle
|
||
|
|
COPY ./drizzle.config.ts /app/drizzle.config.ts
|
||
|
|
COPY ./server /app/server
|
||
|
|
COPY ./database /app/database
|
||
|
|
COPY ./app /app/app
|
||
|
|
# Copy TypeScript configs for tsx runtime
|
||
|
|
COPY ./tsconfig*.json /app/
|
||
|
|
WORKDIR /app
|
||
|
|
# Install tsx in production for running TypeScript
|
||
|
|
RUN npm install tsx
|
||
|
|
CMD ["npm", "run", "start:production"]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 7: Update server.ts Import Path
|
||
|
|
|
||
|
|
After converting socket.js to socket.ts, update the import in server.ts:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Change from:
|
||
|
|
const { initializeSocketIO } = await import("./server/socket.js");
|
||
|
|
|
||
|
|
// To:
|
||
|
|
const { initializeSocketIO } = await import("./server/socket");
|
||
|
|
// Or explicitly:
|
||
|
|
const { initializeSocketIO } = await import("./server/socket.ts");
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 8: Add Timer Functionality (Now Possible!)
|
||
|
|
|
||
|
|
With TypeScript server, you can now properly import database modules:
|
||
|
|
|
||
|
|
**File: `server/timer.ts`**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { database } from "~/database/context";
|
||
|
|
import * as schema from "~/database/schema";
|
||
|
|
import { eq, and } from "drizzle-orm";
|
||
|
|
import { getSocketIO } from "./socket";
|
||
|
|
|
||
|
|
export async function startDraftTimerSystem(): Promise<void> {
|
||
|
|
setInterval(async () => {
|
||
|
|
try {
|
||
|
|
await updateDraftTimers();
|
||
|
|
} catch (error) {
|
||
|
|
console.error("[Timer] Error updating draft timers:", error);
|
||
|
|
}
|
||
|
|
}, 1000);
|
||
|
|
|
||
|
|
console.log("[Timer] Draft timer system started");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function updateDraftTimers(): Promise<void> {
|
||
|
|
const db = database();
|
||
|
|
const io = getSocketIO();
|
||
|
|
|
||
|
|
// Get all active drafts
|
||
|
|
const activeDrafts = await db.query.seasons.findMany({
|
||
|
|
where: eq(schema.seasons.status, "draft"),
|
||
|
|
});
|
||
|
|
|
||
|
|
// ... rest of timer logic
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Then in `server/socket.ts`, add:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { startDraftTimerSystem } from "./timer";
|
||
|
|
|
||
|
|
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||
|
|
// ... existing code ...
|
||
|
|
|
||
|
|
// Start timer system after Socket.IO is initialized
|
||
|
|
startDraftTimerSystem();
|
||
|
|
|
||
|
|
return io;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing Plan
|
||
|
|
|
||
|
|
### 1. Development Mode Testing
|
||
|
|
```bash
|
||
|
|
# Start dev server with TypeScript
|
||
|
|
npm run dev
|
||
|
|
|
||
|
|
# Verify:
|
||
|
|
- Server starts without errors
|
||
|
|
- Vite dev server works
|
||
|
|
- Socket.IO connections work
|
||
|
|
- Hot reload works with tsx watch
|
||
|
|
- Can import database modules
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Production Build Testing
|
||
|
|
```bash
|
||
|
|
# Build the app
|
||
|
|
npm run build
|
||
|
|
|
||
|
|
# Start production server
|
||
|
|
NODE_ENV=production npm start
|
||
|
|
|
||
|
|
# Verify:
|
||
|
|
- Server starts without errors
|
||
|
|
- Static assets are served
|
||
|
|
- Socket.IO works in production
|
||
|
|
- No TypeScript compilation errors
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Docker Testing
|
||
|
|
```bash
|
||
|
|
# Build Docker image
|
||
|
|
docker build -t brackt-test .
|
||
|
|
|
||
|
|
# Run container
|
||
|
|
docker run -p 3000:3000 brackt-test
|
||
|
|
|
||
|
|
# Verify:
|
||
|
|
- Container starts successfully
|
||
|
|
- Application works in containerized environment
|
||
|
|
```
|
||
|
|
|
||
|
|
## Benefits of This Approach
|
||
|
|
|
||
|
|
1. **Type Safety**: Full TypeScript throughout the server
|
||
|
|
2. **Better Imports**: Can import TypeScript modules directly
|
||
|
|
3. **No Build Step**: tsx runs TypeScript directly
|
||
|
|
4. **Fast Development**: tsx watch provides fast restarts
|
||
|
|
5. **Production Ready**: tsx works in production without compilation
|
||
|
|
6. **Maintainable**: Cleaner code with proper types
|
||
|
|
7. **Debugging**: Better stack traces and error messages
|
||
|
|
|
||
|
|
## Migration Checklist
|
||
|
|
|
||
|
|
- [ ] Install tsx and type packages
|
||
|
|
- [ ] Convert server.js to server.ts
|
||
|
|
- [ ] Convert server/socket.js to server/socket.ts
|
||
|
|
- [ ] Create socket.types.ts for event types
|
||
|
|
- [ ] Update package.json scripts
|
||
|
|
- [ ] Update Dockerfile
|
||
|
|
- [ ] Test development mode
|
||
|
|
- [ ] Test production build
|
||
|
|
- [ ] Test Docker build
|
||
|
|
- [ ] Implement timer system with database imports
|
||
|
|
- [ ] Remove old .js files
|
||
|
|
|
||
|
|
## Rollback Plan
|
||
|
|
|
||
|
|
If issues arise:
|
||
|
|
1. Keep original .js files until testing complete
|
||
|
|
2. Can revert package.json scripts to use .js files
|
||
|
|
3. Git commit before starting conversion
|
||
|
|
4. Test thoroughly in development before deploying
|
||
|
|
|
||
|
|
## Notes for Implementation
|
||
|
|
|
||
|
|
- tsx handles TypeScript path aliases (`~/`) automatically
|
||
|
|
- No need for separate compilation step
|
||
|
|
- Works with existing Vite setup
|
||
|
|
- Compatible with React Router v7 SSR
|
||
|
|
- Maintains existing Socket.IO functionality
|
||
|
|
- Enables proper database module imports
|