refactor: migrate server code from JavaScript to TypeScript with build pipeline
This commit is contained in:
parent
9d41508fd0
commit
2bad3dd75e
10 changed files with 1930 additions and 1089 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,3 +6,6 @@
|
|||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
|
||||
# Compiled server output
|
||||
/dist/
|
||||
|
|
|
|||
1543
package-lock.json
generated
1543
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
|
@ -3,13 +3,15 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "react-router build",
|
||||
"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 -- node server.js",
|
||||
"start": "node server.js",
|
||||
"start:production": "drizzle-kit migrate && node server.js",
|
||||
"typecheck": "react-router typegen && tsc -b"
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/react-router": "^2.1.0",
|
||||
|
|
@ -50,17 +52,18 @@
|
|||
"devDependencies": {
|
||||
"@react-router/dev": "^7.7.1",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/express-serve-static-core": "^5.0.6",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.11.14",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"esbuild": "^0.25.11",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsx": "^4.19.2",
|
||||
"tsx": "^4.20.6",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.3",
|
||||
|
|
|
|||
655
plans/server-typescript-conversion-compiled.md
Normal file
655
plans/server-typescript-conversion-compiled.md
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
# 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<void> {
|
||||
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<ClientToServerEvents, ServerToClientEvents> | 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<ClientToServerEvents, ServerToClientEvents>(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<ClientToServerEvents, ServerToClientEvents>) => {
|
||||
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<void> {
|
||||
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
|
||||
493
plans/server-typescript-conversion.md
Normal file
493
plans/server-typescript-conversion.md
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
# 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
|
||||
57
scripts/build-server.mjs
Normal file
57
scripts/build-server.mjs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import * as esbuild from 'esbuild';
|
||||
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 by bundling app code
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, '../app'),
|
||||
'~/database': path.resolve(__dirname, '../database'),
|
||||
},
|
||||
|
||||
// External dependencies (don't bundle node_modules)
|
||||
// But DO bundle our application code (database, app modules)
|
||||
external: [
|
||||
'express',
|
||||
'compression',
|
||||
'morgan',
|
||||
'vite',
|
||||
'socket.io',
|
||||
'drizzle-orm',
|
||||
'postgres',
|
||||
'@clerk/*',
|
||||
'@react-router/*',
|
||||
'../build/server/index.js', // Production build reference (relative to dist/)
|
||||
],
|
||||
|
||||
// Keep import.meta.url working
|
||||
// Note: path, fileURLToPath are imported in source, so we don't duplicate them here
|
||||
banner: {
|
||||
js: `
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
`.trim()
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Server build complete');
|
||||
} catch (error) {
|
||||
console.error('❌ Build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
build();
|
||||
56
server.js
56
server.js
|
|
@ -1,56 +0,0 @@
|
|||
import compression from "compression";
|
||||
import express from "express";
|
||||
import morgan from "morgan";
|
||||
import { createServer } from "http";
|
||||
|
||||
// Short-circuit the type-checking of the built output.
|
||||
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");
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
console.log("Starting development server");
|
||||
const viteDevServer = await import("vite").then((vite) =>
|
||||
vite.createServer({
|
||||
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 (typeof error === "object" && 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" }));
|
||||
app.use(await import(BUILD_PATH).then((mod) => mod.app));
|
||||
}
|
||||
|
||||
// Create HTTP server
|
||||
const httpServer = createServer(app);
|
||||
|
||||
// Initialize Socket.IO
|
||||
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}`);
|
||||
});
|
||||
87
server.ts
Normal file
87
server.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
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";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// ESM module resolution helpers
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Configuration
|
||||
// Resolve paths relative to project root (one level up from dist/ in production, or current dir in dev)
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
||||
const BUILD_PATH = path.join(PROJECT_ROOT, "build/server/index.js");
|
||||
const CLIENT_BUILD_PATH = path.join(PROJECT_ROOT, "build/client");
|
||||
const CLIENT_ASSETS_PATH = path.join(PROJECT_ROOT, "build/client/assets");
|
||||
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 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(CLIENT_ASSETS_PATH, {
|
||||
immutable: true,
|
||||
maxAge: "1y"
|
||||
})
|
||||
);
|
||||
|
||||
app.use(morgan("tiny"));
|
||||
app.use(express.static(CLIENT_BUILD_PATH, { 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
|
||||
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);
|
||||
});
|
||||
|
|
@ -1,36 +1,62 @@
|
|||
import { Server as SocketIOServer } from "socket.io";
|
||||
import { Server as SocketIOServer, Socket } from "socket.io";
|
||||
import type { Server as HTTPServer } from "http";
|
||||
|
||||
/** @type {SocketIOServer | null} */
|
||||
let io = null;
|
||||
// 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<ClientToServerEvents, ServerToClientEvents> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize Socket.IO server
|
||||
* @param {import('http').Server} httpServer
|
||||
* @returns {SocketIOServer}
|
||||
*/
|
||||
export function initializeSocketIO(httpServer) {
|
||||
export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
||||
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
|
||||
// Create typed Socket.IO server
|
||||
io = new SocketIOServer<ClientToServerEvents, ServerToClientEvents>(httpServer, {
|
||||
cors: process.env.NODE_ENV === "production" && process.env.APP_URL
|
||||
? {
|
||||
origin: process.env.APP_URL,
|
||||
credentials: true,
|
||||
}
|
||||
: undefined, // No CORS in dev (same origin)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Connection handling
|
||||
io.on("connection", (socket) => {
|
||||
io.on("connection", (socket: Socket<ClientToServerEvents, ServerToClientEvents>) => {
|
||||
console.log("Client connected:", socket.id);
|
||||
|
||||
// Join draft room
|
||||
socket.on("join-draft", (seasonId) => {
|
||||
socket.on("join-draft", (seasonId: string) => {
|
||||
if (!seasonId) {
|
||||
console.error("No seasonId provided for join-draft");
|
||||
return;
|
||||
|
|
@ -39,18 +65,15 @@ export function initializeSocketIO(httpServer) {
|
|||
console.log(`Socket ${socket.id} joined draft-${seasonId}`);
|
||||
});
|
||||
|
||||
// Leave draft room
|
||||
socket.on("leave-draft", (seasonId) => {
|
||||
socket.on("leave-draft", (seasonId: string) => {
|
||||
if (!seasonId) return;
|
||||
socket.leave(`draft-${seasonId}`);
|
||||
console.log(`Socket ${socket.id} left draft-${seasonId}`);
|
||||
});
|
||||
|
||||
// Test event handler - echo back with server timestamp
|
||||
socket.on("test-event", (data) => {
|
||||
socket.on("test-event", (data: any) => {
|
||||
console.log("📨 Received test-event from client:", socket.id, data);
|
||||
|
||||
// Echo back to the client with server response
|
||||
socket.emit("test-message", {
|
||||
originalMessage: data,
|
||||
serverResponse: "Hello from server!",
|
||||
|
|
@ -66,8 +89,7 @@ export function initializeSocketIO(httpServer) {
|
|||
});
|
||||
});
|
||||
|
||||
// Store globally for access in route handlers
|
||||
// This is safe because server.js controls the lifecycle
|
||||
// Store globally for route handlers
|
||||
global.__socketIO = io;
|
||||
|
||||
console.log("Socket.IO initialized");
|
||||
|
|
@ -76,10 +98,8 @@ export function initializeSocketIO(httpServer) {
|
|||
|
||||
/**
|
||||
* Get the Socket.IO server instance
|
||||
* @returns {SocketIOServer}
|
||||
*/
|
||||
export function getSocketIO() {
|
||||
// Check both local and global storage
|
||||
export function getSocketIO(): SocketIOServer {
|
||||
const instance = io || global.__socketIO;
|
||||
if (!instance) {
|
||||
throw new Error("Socket.IO not initialized. Call initializeSocketIO first.");
|
||||
38
tsconfig.server.json
Normal file
38
tsconfig.server.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue