# Server TypeScript Conversion Plan (With Compilation) ## Overview Convert the Node.js server from JavaScript to TypeScript with a build step for production. This approach uses tsx for development (fast iteration) but compiles to optimized JavaScript for production (zero runtime overhead). ## Key Differences from TSX-Only Approach - **Development**: Uses `tsx` for fast iteration and hot reload - **Production**: Compiles to JavaScript, runs pure Node.js (no TypeScript overhead) - **Build Process**: Adds a server compilation step - **Docker**: Smaller production image (no tsx runtime needed) ## Current State Analysis ### Files to Convert 1. **`server.js`** (57 lines) - Main server entry point 2. **`server/socket.js`** (89 lines) - Socket.IO server ### 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 ## Implementation Steps ### Step 1: Install Required Dependencies ```bash # Development dependencies npm install --save-dev tsx @types/express @types/morgan @types/compression esbuild # Why these packages? # - tsx: Fast TypeScript execution for development # - @types/*: TypeScript definitions # - esbuild: Ultra-fast TypeScript compiler for production builds ``` ### Step 2: Create Server TypeScript Configuration **File: `tsconfig.server.json`** ```json { "extends": "./tsconfig.json", "compilerOptions": { "composite": false, "noEmit": false, "outDir": "./dist/server", "rootDir": ".", "target": "ES2022", "module": "ES2022", "moduleResolution": "node", "lib": ["ES2022"], "types": ["node"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", "paths": { "~/*": ["./app/*"], "~/database/*": ["./database/*"] } }, "include": [ "server.ts", "server/**/*.ts", "database/**/*.ts", "app/database/**/*.ts" ], "exclude": [ "node_modules", "build", "dist", "**/*.test.ts", "**/*.spec.ts" ] } ``` ### Step 3: Create Build Script for Server **File: `scripts/build-server.mjs`** ```javascript import * as esbuild from 'esbuild'; import { nodeExternalsPlugin } from 'esbuild-node-externals'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function build() { try { await esbuild.build({ entryPoints: ['./server.ts'], bundle: true, platform: 'node', target: 'node20', format: 'esm', outfile: 'dist/server.js', sourcemap: true, minify: process.env.NODE_ENV === 'production', // Handle TypeScript path aliases alias: { '~': path.resolve(__dirname, '../app'), '~/database': path.resolve(__dirname, '../database'), }, // External dependencies (don't bundle node_modules) external: [ 'express', 'compression', 'morgan', 'vite', 'socket.io', 'drizzle-orm', 'postgres', '@clerk/*', './build/server/index.js', // Production build reference ], // Keep import.meta.url working banner: { js: ` import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import path from 'path'; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); `.trim() } }); console.log('✅ Server build complete'); } catch (error) { console.error('❌ Build failed:', error); process.exit(1); } } build(); ``` ### Step 4: Convert server.js to server.ts **File: `server.ts`** ```typescript import compression from "compression"; import express, { Express, Request, Response, NextFunction } from "express"; import morgan from "morgan"; import { createServer } from "http"; import type { ViteDevServer } from "vite"; // 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 { const app: Express = express(); app.use(compression()); app.disable("x-powered-by"); if (DEVELOPMENT) { console.log("Starting development server"); // Dynamic import Vite only in development const { createServer: createViteServer } = await import("vite"); const viteDevServer: ViteDevServer = await createViteServer({ server: { middlewareMode: true }, }); app.use(viteDevServer.middlewares); app.use(async (req: Request, res: Response, next: NextFunction) => { 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" })); // Import the built React Router app const { app: productionApp } = await import(BUILD_PATH); app.use(productionApp); } // Create HTTP server const httpServer = createServer(app); // Initialize Socket.IO - will be compiled to .js in production const { initializeSocketIO } = await import("./server/socket"); initializeSocketIO(httpServer); // Start server httpServer.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); } // Start the server with error handling createAppServer().catch((error) => { console.error("Failed to start server:", error); process.exit(1); }); ``` ### Step 5: Convert server/socket.js to server/socket.ts **File: `server/socket.ts`** ```typescript import { Server as SocketIOServer, Socket } from "socket.io"; import type { Server as HTTPServer } from "http"; // Socket event types 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; } interface ClientToServerEvents { "join-draft": (seasonId: string) => void; "leave-draft": (seasonId: string) => void; "test-event": (data: any) => void; } // Global type augmentation 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 typed Socket.IO server io = new SocketIOServer(httpServer, { cors: process.env.NODE_ENV === "production" && process.env.APP_URL ? { origin: process.env.APP_URL, credentials: true, } : undefined, }); // Connection handling io.on("connection", (socket: Socket) => { console.log("Client connected:", socket.id); 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}`); }); socket.on("leave-draft", (seasonId: string) => { if (!seasonId) return; socket.leave(`draft-${seasonId}`); console.log(`Socket ${socket.id} left draft-${seasonId}`); }); 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 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 6: Update package.json Scripts ```json { "scripts": { "build": "npm run build:remix && npm run build:server", "build:remix": "react-router build", "build:server": "node scripts/build-server.mjs", "db:generate": "dotenv -- drizzle-kit generate", "db:migrate": "dotenv -- drizzle-kit migrate", "dev": "dotenv -- tsx watch server.ts", "start": "node dist/server.js", "start:production": "drizzle-kit migrate && node dist/server.js", "typecheck": "react-router typegen && tsc -b && tsc -p tsconfig.server.json --noEmit" } } ``` **Key Changes:** - `build`: Now builds both React Router app AND server - `build:server`: Compiles TypeScript server to JavaScript - `dev`: Uses `tsx watch` for development (fast, no compilation) - `start`: Runs compiled JavaScript (no TypeScript overhead) - `typecheck`: Also checks server TypeScript ### Step 7: Update Dockerfile (Optimized for Production) ```dockerfile # Stage 1: Install all dependencies FROM node:20-alpine AS dependencies WORKDIR /app COPY package*.json ./ RUN npm ci # Stage 2: Build the application FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ COPY --from=dependencies /app/node_modules ./node_modules COPY . . # Build both React Router app and server RUN npm run build # Stage 3: Production dependencies only FROM node:20-alpine AS prod-dependencies WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev # Stage 4: Final production image (minimal) FROM node:20-alpine WORKDIR /app # Copy only what's needed for production COPY package*.json ./ COPY --from=prod-dependencies /app/node_modules ./node_modules COPY --from=builder /app/build ./build COPY --from=builder /app/dist ./dist COPY ./drizzle ./drizzle COPY ./drizzle.config.ts ./drizzle.config.ts # No TypeScript files needed in production! # The server runs pure JavaScript from dist/server.js EXPOSE 3000 CMD ["npm", "run", "start:production"] ``` ### Step 8: Create Timer System (With Proper Imports) **File: `server/timer.ts`** ```typescript import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, desc, asc, inArray, notInArray } from "drizzle-orm"; import { getSocketIO } from "./socket"; let timerInterval: NodeJS.Timeout | null = null; export function startDraftTimerSystem(): void { if (timerInterval) { clearInterval(timerInterval); } timerInterval = 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 { const db = database(); const io = getSocketIO(); // Get all active drafts const activeDrafts = await db.query.seasons.findMany({ where: eq(schema.seasons.status, "draft"), }); for (const season of activeDrafts) { if (season.draftPaused) continue; const currentPickNumber = season.currentPickNumber || 1; // Get draft slots const draftSlots = await db.query.draftSlots.findMany({ where: eq(schema.draftSlots.seasonId, season.id), orderBy: schema.draftSlots.draftOrder, }); // Calculate current team const totalTeams = draftSlots.length; const currentRound = Math.ceil(currentPickNumber / totalTeams); const isEvenRound = currentRound % 2 === 0; let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1; if (isEvenRound) { pickInRound = totalTeams - pickInRound + 1; } const currentDraftSlot = draftSlots.find( (slot) => slot.draftOrder === pickInRound ); if (!currentDraftSlot) continue; // Update timer const timer = await db.query.draftTimers.findFirst({ where: and( eq(schema.draftTimers.seasonId, season.id), eq(schema.draftTimers.teamId, currentDraftSlot.teamId) ), }); if (!timer) continue; const newTimeRemaining = Math.max(0, timer.timeRemaining - 1); // Update in database await db .update(schema.draftTimers) .set({ timeRemaining: newTimeRemaining, updatedAt: new Date(), }) .where(eq(schema.draftTimers.id, timer.id)); // Broadcast update io.to(`draft-${season.id}`).emit("timer-update", { seasonId: season.id, teamId: currentDraftSlot.teamId, timeRemaining: newTimeRemaining, currentPickNumber, }); // Trigger auto-pick if timer expired if (newTimeRemaining === 0 && timer.timeRemaining > 0) { console.log(`[Timer] Timer expired for team ${currentDraftSlot.teamId}`); // Auto-pick logic here... } } } ``` ### Step 9: Update .gitignore Add these entries: ``` # Compiled server output /dist dist/ # Keep source TypeScript files !server.ts !server/**/*.ts ``` ## Alternative: Use Vite to Build Server If you prefer to use Vite for consistency, create: **File: `vite.config.server.ts`** ```typescript import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ build: { ssr: true, target: 'node20', outDir: 'dist', rollupOptions: { input: './server.ts', external: [ /^node:/, 'express', 'compression', 'morgan', 'vite', 'socket.io', 'drizzle-orm', 'postgres', ], output: { format: 'es', }, }, }, plugins: [tsconfigPaths()], ssr: { noExternal: ['~/database', '~/app'], }, }); ``` Then update build script: ```json "build:server": "vite build --config vite.config.server.ts" ``` ## Testing Plan ### 1. Development Testing ```bash # Start with tsx (fast, no compilation) npm run dev # Verify: - Server starts quickly - Hot reload works - Can import TypeScript modules - Socket.IO connects ``` ### 2. Build Testing ```bash # Build server npm run build:server # Check output ls -la dist/ # Should see: server.js, server.js.map # Test compiled server NODE_ENV=production npm start # Verify: - Runs pure JavaScript (no tsx) - All features work - No TypeScript overhead ``` ### 3. Performance Comparison ```bash # Measure startup time with tsx time npm run dev # Measure startup time with compiled JS time npm start # Compiled version should be faster ``` ## Benefits of This Approach 1. **Development Speed**: tsx provides fast iteration in development 2. **Production Performance**: Zero TypeScript overhead in production 3. **Type Safety**: Full TypeScript benefits during development 4. **Smaller Docker Image**: No tsx or TypeScript in production container 5. **Faster Startup**: Compiled JavaScript starts faster than tsx 6. **Debugging**: Source maps available in both dev and production ## Migration Checklist - [ ] Install dependencies (tsx, esbuild, types) - [ ] Create tsconfig.server.json - [ ] Create build-server.mjs script - [ ] Convert server.js to server.ts - [ ] Convert server/socket.js to server/socket.ts - [ ] Update package.json scripts - [ ] Test development mode with tsx - [ ] Test build process - [ ] Test production with compiled JS - [ ] Update Dockerfile - [ ] Update .gitignore - [ ] Implement timer system - [ ] Remove old .js files ## Performance Metrics Expected improvements: - **Startup time**: 30-50% faster (no TypeScript compilation) - **Memory usage**: 20-30% less (no tsx runtime) - **CPU usage**: Lower (no runtime transpilation) - **Docker image size**: ~50MB smaller (no TypeScript dependencies) ## Rollback Plan 1. Keep original .js files until testing complete 2. Git commit before conversion 3. Can revert package.json to use .js files 4. Compiled output is separate from source