* 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>
7.5 KiB
Phase 3: Socket.ts Implementation Plan - Avoiding Build Issues
The Problem
React Router v7 bundles server/app.ts into build/server/index.js. If we're not careful, Socket.IO code could:
- Get bundled into the server build (causing issues)
- Try to import from bundled code (import path problems)
- Create circular dependencies
- Break HMR in development
Solution Strategy
Option 1: Keep Socket.IO OUTSIDE the Bundle (Recommended) ✅
Structure:
/server.js <- Entry point (not bundled)
/server/
socket.js <- Socket.IO code (not bundled, plain JS)
app.ts <- React Router app (gets bundled)
Why this works:
server.jsis the entry point and NOT bundled by React Routerserver/socket.jsis imported byserver.js, not by React Router code- No circular dependencies
- Socket.IO stays completely separate from React Router's build
Option 2: Use Dynamic Import (Alternative)
Keep Socket.IO in TypeScript but use dynamic imports to prevent bundling:
// In server.js
const { initializeSocketIO } = await import("./server/socket.js");
This ensures Socket.IO is loaded at runtime, not bundled.
Detailed Implementation Plan
Step 1: Create server/socket.js (JavaScript, not TypeScript)
File: server/socket.js
// Use CommonJS or ES modules based on your Node version
import { Server as SocketIOServer } from "socket.io";
let io = null;
export function initializeSocketIO(httpServer) {
if (io) {
console.log("Socket.IO already initialized");
return io;
}
// Create Socket.IO server
io = new SocketIOServer(httpServer, {
// CORS only needed if clients connect from different origin
cors: process.env.NODE_ENV === "production" && process.env.APP_URL
? {
origin: process.env.APP_URL,
credentials: true,
}
: undefined, // No CORS in dev (same origin)
});
// Connection handling
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
// Join draft room
socket.on("join-draft", (seasonId) => {
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) => {
if (!seasonId) return;
socket.leave(`draft-${seasonId}`);
console.log(`Socket ${socket.id} left draft-${seasonId}`);
});
socket.on("disconnect", () => {
console.log("Client disconnected:", socket.id);
});
});
// Store globally for access in route handlers
// This is safe because server.js controls the lifecycle
global.__socketIO = io;
console.log("Socket.IO initialized on", httpServer.address());
return io;
}
export function getSocketIO() {
// Check both local and global storage
const instance = io || global.__socketIO;
if (!instance) {
throw new Error("Socket.IO not initialized. Call initializeSocketIO first.");
}
return instance;
}
Step 2: Create TypeScript definitions
File: server/socket.d.ts (Type definitions for socket.js)
import type { Server as HTTPServer } from "http";
import type { Server as SocketIOServer } from "socket.io";
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer;
export function getSocketIO(): SocketIOServer;
File: server/types.d.ts (Global type augmentation)
import type { Server as SocketIOServer } from "socket.io";
declare global {
var __socketIO: SocketIOServer | undefined;
}
export {};
Step 3: Update server.js
import compression from "compression";
import express from "express";
import morgan from "morgan";
import { createServer } from "http";
const BUILD_PATH = "./build/server/index.js";
const DEVELOPMENT = process.env.NODE_ENV === "development";
const PORT = Number.parseInt(process.env.PORT || "3000");
const app = express();
app.use(compression());
app.disable("x-powered-by");
// ... existing middleware setup ...
// Create HTTP server
const httpServer = createServer(app);
// Initialize Socket.IO AFTER creating HTTP server
// Dynamic import to ensure it's not bundled
const { initializeSocketIO } = await import("./server/socket.js");
initializeSocketIO(httpServer);
// Use httpServer.listen instead of app.listen
httpServer.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Step 4: Access Socket.IO from React Router Routes
Option A: Direct Import (May have path issues)
// In app/routes/api.draft-pick.ts
import { getSocketIO } from "../../server/socket.js";
export async function action({ request }: ActionFunctionArgs) {
// Make database changes...
// Broadcast update
const io = getSocketIO();
io.to(`draft-${seasonId}`).emit("pick-made", data);
return json({ success: true });
}
Option B: Through Context (Cleaner but more setup)
// In server/app.ts
import { getSocketIO } from "./socket.js";
// Add to loader context
export function getLoadContext() {
return {
getSocketIO, // Pass function reference
};
}
// In route
export async function action({ request, context }: ActionFunctionArgs) {
const io = context.getSocketIO();
io.to(`draft-${seasonId}`).emit("pick-made", data);
}
Build Process Verification
Development Mode Testing
npm run dev
# Check console for "Socket.IO initialized"
# Verify no build errors
# Test HMR still works
Production Build Testing
npm run build
# Check build/server/index.js
# Socket.IO code should NOT be in the bundle
# Only server/app.ts code should be bundled
# Run production
NODE_ENV=production node server.js
# Verify Socket.IO initializes
Potential Issues & Solutions
Issue 1: Import path resolution
Problem: Can't import server/socket.js from React Router routes
Solution: Use global access pattern or pass through context
Issue 2: TypeScript errors
Problem: TS can't find types for server/socket.js
Solution: Create server/socket.d.ts with type definitions
Issue 3: HMR breaks Socket.IO
Problem: Hot reloads create multiple Socket.IO instances
Solution: The if (io) check prevents re-initialization
Issue 4: Build includes Socket.IO
Problem: Socket.IO gets bundled into build/server/index.js Solution: Keep as .js file, use dynamic import in server.js
Why .js Instead of .ts?
- No bundling: .js files aren't processed by React Router's build
- Simple imports: No build step needed for server.js to import it
- Clear separation: Socket.IO logic separate from React Router app
- Runtime loading: Loaded when server.js runs, not at build time
Alternative: Using TypeScript
If you really want TypeScript for Socket.IO:
- Create
server/socket.ts - Add a separate build step:
tsc server/socket.ts --outDir dist - Import from
dist/socket.jsin server.js - Add to package.json scripts:
"build:socket": "tsc server/socket.ts --outDir dist", "build": "npm run build:socket && react-router build"
But this adds complexity for minimal benefit.
Recommended Approach
✅ Use server/socket.js (plain JavaScript)
- Simpler
- No build step
- No bundling issues
- Still get type safety through .d.ts files
- Industry standard approach
This keeps Socket.IO completely separate from React Router's build process while maintaining type safety and avoiding all bundling issues.