* Update claude.md to push more tests. * Archiving old plans. * fix: run DB migrations programmatically on server startup Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in migrator running in the server process before accepting connections. This ensures migrations are always applied on deploy and fails fast if they error. - Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator - Skip migrations in development (handled manually via db:migrate) - Add DATABASE_URL guard with a clear error message - Remove start:production script (now identical to start) - Update Dockerfile CMD to use npm run start Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
13 KiB
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
-
server.js(57 lines) - Main server entry point- Handles Vite dev server setup
- Production build serving
- Socket.IO initialization
- Express middleware
-
server/socket.js(89 lines) - Socket.IO server- Connection handling
- Draft room management
- Global instance storage
Existing TypeScript Configuration
tsconfig.node.jsonalready includesserver/**/*.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/nodeis configured in tsconfig.node.json- Vite handles TypeScript compilation in development
Implementation Steps
Step 1: Install Required Dependencies
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
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
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
// 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:
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
{
"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: Usestsx watchfor auto-restart on file changesstart: Usestsxto run TypeScript directlystart:production: Same as start but with migration
Step 6: Update 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:
// 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
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:
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
# 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
# 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
# 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
- Type Safety: Full TypeScript throughout the server
- Better Imports: Can import TypeScript modules directly
- No Build Step: tsx runs TypeScript directly
- Fast Development: tsx watch provides fast restarts
- Production Ready: tsx works in production without compilation
- Maintainable: Cleaner code with proper types
- 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:
- Keep original .js files until testing complete
- Can revert package.json scripts to use .js files
- Git commit before starting conversion
- 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