1
0

Updated docker and server configs

This commit is contained in:
James Ketr 2025-09-23 12:26:34 -07:00
parent eb39a175df
commit b553cdc656
17 changed files with 511 additions and 149 deletions

View File

@ -1,5 +1,7 @@
* *
!server !server/
server/node_modules !client/
server/node_modules/
client/node_modules/
!Dockerfile !Dockerfile
!.env !.env

48
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,48 @@
Do not run npm or create local node_modules in this repository
============================================================
This repository is designed to run inside containers. Do NOT run `npm install`,
`npm ci`, or otherwise modify `node_modules` or lockfiles locally on your
machine. Doing so will create environment drift and can break container-based
builds and CI.
Guidelines for contributors and automated assistants
- Use the provided Docker and docker-compose workflows for development and
building. The repository's `Dockerfile` and `docker-compose.yml` are the
canonical way to install dependencies and run the app.
- If you need to run a command locally for quick checks, prefer run commands in
the project's container environment. Example (copy-paste):
```bash
docker compose run --rm peddlers-of-ketran bash
# inside container
npm install
npm run build
```
- For lightweight commands, you can run one-off containers:
```bash
docker run --rm -v "$PWD":/app -w /app node:20 bash -lc 'cd client && npm ci && npm run type-check'
```
- Do not suggest or perform `npm install` or `npm ci` on the host machine in
repository files, PRs, or automated edits. If a change requires dependency
updates, update package.json and lockfiles via a CI job or in-container run,
and record the change in a PR.
Copilot / Automation specific hints
- When editing package.json, also update the appropriate lockfile using the
container workflow and include lockfile changes in the same PR.
- If a change requires running tests or builds, prefer to add a GitHub Action
that runs the command inside the standard container image rather than
instructing contributors to run commands locally.
Why this matters
- Containers provide a reproducible environment and prevent developer
machines from diverging. They ensure CI and production builds are consistent.
If you need a recommended local development setup or a dev container (VS Code
Remote - Containers), create a `.devcontainer/` configuration and include it in
the repo. That provides a supported local dev workflow that still uses
containerized tooling.

32
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: CI
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
server-build:
runs-on: ubuntu-latest
name: Server build and type-check (container)
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Server: Type-check and Build (inside node:20)
run: |
docker run --rm -v "$PWD":/work -w /work node:20 bash -lc '
cd server && npm ci --no-audit --no-fund && npm run type-check && npm run build'
client-build:
runs-on: ubuntu-latest
name: Client type-check and build (container)
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Client: Type-check and Build (inside node:20)
run: |
docker run --rm -v "$PWD":/work -w /work node:20 bash -lc '
cd client && npm ci --no-audit --no-fund && npm run type-check && npm run build'

10
.gitignore vendored
View File

@ -1,4 +1,12 @@
!.gitignore /.ssh/
/client/node_modules/
/server/node_modules/
/node_modules/
/dist/
/build/
.env
.DS_Store
/.vscode/!.gitignore
!package.json !package.json
node_modules node_modules
package-lock.json package-lock.json

View File

@ -1,4 +1,4 @@
FROM ubuntu:jammy FROM ubuntu:noble
RUN apt-get -q update \ RUN apt-get -q update \
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
@ -10,7 +10,7 @@ RUN apt-get -q update \
RUN mkdir -p /etc/apt/keyrings RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
ENV NODE_MAJOR=20 ENV NODE_MAJOR=22
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get -q update \ RUN apt-get -q update \
@ -25,12 +25,26 @@ RUN apt-get -q update \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} && rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
COPY /server /server COPY server /server
WORKDIR /server WORKDIR /server
RUN npm install -s sqlite3 RUN npm install -s sqlite3
RUN npm install RUN npm install
RUN npm run build
# prepare client deps in the image so lint/type-check can run inside the container
# copy client sources and install dependencies during the image build (container-first)
COPY client /client
WORKDIR /client
ENV PUBLIC_URL="/ketr.ketran"
ENV REACT_APP_API_BASE="/ketr.ketran"
# prefer npm ci when lockfile present, otherwise fall back to npm install
RUN rm -f package-lock.json
RUN npm install --legacy-peer-deps --no-audit --no-fund
RUN npm run build
# return to server working dir for default run
WORKDIR /server
COPY /Dockerfile /Dockerfile COPY /Dockerfile /Dockerfile
COPY /.env /.env COPY /.env /.env
ENTRYPOINT [ "npm", "start" ] CMD ["npm", "start"]

26
Dockerfile.server Normal file
View File

@ -0,0 +1,26 @@
## Multi-stage Dockerfile for building the TypeScript server
FROM node:20-alpine AS builder
WORKDIR /
# Copy package files and install (in container only)
COPY server/package*.json ./server/
WORKDIR /server
RUN npm ci --silent
# Copy server source and build
COPY server/ ./
RUN npm run build
## Production image
FROM node:20-alpine AS runtime
WORKDIR /
# Copy built server
COPY --from=builder /server/dist ./server/dist
COPY server/package*.json /server/
WORKDIR /server
ENV NODE_ENV=production
EXPOSE 8930
CMD ["npm", "start"]

239
README.md
View File

@ -1,39 +1,115 @@
# Peddlers of Ketran # Peddlers of Ketran
This project consists of both the front-end and back-end game This project consists of both the front-end React client and back-end Node.js game API server for the Settlers of Catan-style board game.
API server.
The front-end is launched from the 'client' directory in development mode via 'npm start'. In production, you build The application is designed to run inside Docker containers. Do not run `npm install` or modify `node_modules` locally, as this can create environment drift. All development and building should be done through the provided Docker workflows.
it via 'npm build' and deploy the public front-end.
The back-end is launched out of the 'server' directory via 'npm start' and will ## Development
bind to the default port 8930.
If you change the default port of the REST API server, you will need to change For live development with hot reloading, React DevTools support, and TypeScript compilation on-the-fly.
client/package.json's "proxy" value to reflect the new port change.
NOTE: ### Prerequisites
Board.js currently hard codes assetsPath and gamesPath to be absolute as the - Docker
dynamic router and resource / asset loading isn't working correctly. - Docker Compose
## Building ### Running the Server (Backend)
### Native The server uses TypeScript with hot reloading via `ts-node-dev`.
#### Prerequisites
```bash ```bash
sudo apt-get install -y nodejs npm python docker compose -f docker-compose.dev.yml up
sudo -E npm install --global npm@latest
``` ```
### In container This will:
- Build the server container using `Dockerfile.server`
- Mount the `server/` directory for live code changes
- Run `npm run start:dev` which uses `ts-node-dev` for hot reloading
- Expose the server on port 8930
### Testing the Server
The server uses Jest for unit testing. Tests are located in `server/tests/`.
To run tests:
```bash ```bash
docker compose run --rm server npm test
``` ```
# Architecture This will run all test files matching `*.test.ts` in the `server/tests/` directory.
To run tests in watch mode:
```bash
docker compose run --rm server npm run test:watch
```
Note: If you add `test:watch` script, but for now, it's just `test`.
### Running the Client (Frontend)
The client is a React application with hot reloading and debug support.
```bash
docker compose -f docker-compose.client.dev.yml up
```
This will:
- Mount the `client/` directory for live code changes
- Run `npm start` in development mode
- Enable React DevTools and hot module replacement
- Expose the client on port 3001 (HTTPS)
- Proxy API calls to the server at `http://host.docker.internal:8930`
### Full Development Stack
To run both server and client together:
```bash
# Terminal 1: Server
docker compose -f docker-compose.dev.yml up
# Terminal 2: Client
docker compose -f docker-compose.client.dev.yml up
```
Open `https://localhost:3001` in your browser. The client will hot-reload on code changes, and the server will restart automatically on backend changes.
## Production
For production deployment, the client is built as static files and served directly by the Node.js server.
### Building
```bash
docker compose build
```
This builds the production image using `Dockerfile`, which:
- Installs server dependencies and builds TypeScript to `dist/`
- Installs client dependencies and builds static files to `client/build/`
- Copies the built client files to be served by the server
### Running
```bash
docker compose up
```
This will:
- Start the server on port 8930
- Serve the built client static files
- Mount the `db/` directory for persistent database storage
- Mount `server/routes/` for dynamic route files
The application will be available at `http://localhost:8930`.
### Environment Variables
Create a `.env` file in the project root with any required environment variables. The server start script loads these via `export $(cat ../.env | xargs)`.
## Architecture
```plantuml ```plantuml
skinparam componentStyle rectangle skinparam componentStyle rectangle
@ -48,99 +124,40 @@ package "Game" as game {
component Players as players component Players as players
} }
server <-> resource : serves to client server <-> resource : serves static files to client
client <-> server client <-> server : API calls
player <-> client player <-> client
players -r-> player players -r-> player
server -> game server -> game
``` ```
# Ketr.Ketran REST API ## API Documentation
## POST /api/v1/game ### POST /api/v1/game
### Request #### Request
```json ```json
{} {}
``` ```
### Response #### Response
```json ```json
{ {
gameId: id "gameId": "id",
gameState: { "gameState": {
tiles: [] "tiles": []
} }
} }
``` ```
# Configuring / installing ## Configuration
## Build Add security tokens in `server/config/local.json`:
```bash ```bash
git clone git.ketrenos.com:jketreno/peddlers-of-ketran.git cat << EOF > server/config/local.json
cd server
npm install
npm start &
cd ../client
npm install
npm start
```
## Install
```bash
export BASEPATH=${PWD}
# Ensure sudo has password ready
sudo -l
sudo rsync -avprl install/ /etc/
sudo touch /var/log/ketr-ketran.log
sudo chown system:adm /var/log/ketr-ketran.log
sudo systemctl daemon-reload
sudo systemctl restart rsyslogd
sudo systemctl restart logrotate
sudo systemctl restart ketr.ketran
```
Install the following into your nginx server configuration:
```nginx
location /ketr.ketran {
root /var/www/ketrenos.com;
index unresolvable-file-html.html;
try_files $uri @index;
}
# This seperate location is so the no cache policy only applies to the index and nothing else.
location @index {
root /var/www/ketrenos.com/ketr.ketran;
add_header Cache-Control no-cache;
expires 0;
try_files /index.html =404;
}
location /ketr.ketran/api {
proxy_pass http://192.168.1.78:8930;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass_header Set-Cookie;
proxy_pass_header P3P;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
Add security tokens in ketr.ketran/config/local.json:
```bash
cat << EOF > config/local.json
{ {
"tokens": [ { "tokens": [ {
"$(whoami)": "$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)" "$(whoami)": "$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)"
@ -149,45 +166,25 @@ cat << EOF > config/local.json
EOF EOF
``` ```
## Launch ## Testing
### Create New Game
```bash ```bash
sudo systemctl start ketr.ketran curl -X POST http://localhost:8930/api/v1/games/
``` ```
## To test ### Get Game Status
### New game
```bash ```bash
curl -k -s -X POST http://localhost:8930/ketr.ketran/api/v1/games/ curl -X GET http://localhost:8930/api/v1/games/:id
``` ```
### Game status ## Game States
```bash - **Lobby**: Players register, choose colors, roll for position, set ready
curl -k -s -X GET http://localhost:8930/ketr.ketran/api/v1/games/:id - **Active**: Gameplay phase
```
# States Chat is available at all times for registered players.
Chat is available at all times by registered players ## License Attribution
## Lobby The dice faces (dice-six-faces-*.svg) are Copyright [https://delapouite.com/](https://delapouite.com/) and licensed as [CC-BY-3.0](https://creativecommons.org/licenses/by/3.0/).
* Register session+name
* Register session with color
* Unregister player+name from color
* Roll dice for player position
* Shuffle board
* Set "Ready" for player
* All ready? state == active
## License attribution
The dice faces (dice-six-faces-*.svg) are Copyright https://delapouite.com/ and licensed
as [CC-BY-3.0](https://creativecommons.org/licenses/by/3.0/).
## Active
*

View File

@ -0,0 +1,22 @@
services:
peddlers-client:
build:
context: .
dockerfile: Dockerfile
container_name: peddlers-client
working_dir: /client
volumes:
- ./client:/client:rw
ports:
- 3001:3000
environment:
- BROWSER=none
- HTTPS=true
- HOST=0.0.0.0
command: ["bash", "-c", "cd /client && npm install --legacy-peer-deps --silent --no-audit --no-fund && npm start"]
networks:
- peddlers-network
networks:
peddlers-network:
driver: bridge

21
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,21 @@
services:
peddlers-of-ketran:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- ./server:/server:rw
- ./db:/db:rw
#- ./server/node_modules:app/server/node_modules:rw
command: ["sh", "-c", "cd /server && npm install --no-audit --no-fund --silent && npm run start:dev"]
ports:
- 8930:8930
environment:
- NODE_ENV=development
tty: true
networks:
- peddlers-network
networks:
peddlers-network:
driver: bridge

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
services:
peddlers-of-ketran:
container_name: ketr.ketran
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- 8930:8930
volumes:
- ./db:/db:rw
- ./server/routes:/server/routes:ro
working_dir: /server
peddlers-client:
container_name: ketr.client
build:
context: .
dockerfile: Dockerfile
restart: 'no'
working_dir: /client
volumes:
- ./client:/client:rw
#- ./client:/client/node_modules:rw
tty: true
stdin_open: true

View File

@ -1,4 +1,4 @@
let basePath = process.env.REACT_APP_basePath; let basePath = process.env.REACT_APP_basePath || "";
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/"; basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
if (basePath == "//") { if (basePath == "//") {
basePath = "/"; basePath = "/";

View File

@ -1 +1,3 @@
{} {
"frontendPath": "/client/build"
}

View File

@ -3,7 +3,12 @@
"version": "1.0.0", "version": "1.0.0",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "export $(cat ../.env | xargs) && node app.js" "start": "export $(cat ../.env | xargs) && node dist/app.js",
"start:legacy": "export $(cat ../.env | xargs) && node app.js",
"build": "tsc -p tsconfig.json",
"start:dev": "ts-node-dev --respawn --transpile-only src/app.ts",
"test": "jest",
"type-check": "tsc -p tsconfig.json --noEmit"
}, },
"author": "James Ketrenos <james_settlers@ketrenos.com>", "author": "James Ketrenos <james_settlers@ketrenos.com>",
"license": "MIT", "license": "MIT",
@ -30,6 +35,19 @@
"typeface-roboto": "0.0.75", "typeface-roboto": "0.0.75",
"ws": "^8.5.0" "ws": "^8.5.0"
}, },
"devDependencies": {
"@types/jest": "^29.5.0",
"@types/node": "^20.0.0",
"@types/supertest": "^2.0.12",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
},
"jest": {
"preset": "ts-jest"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@git.ketrenos.com:jketreno/peddlers-of-ketran" "url": "git@git.ketrenos.com:jketreno/peddlers-of-ketran"

View File

@ -2,8 +2,8 @@
const express = require("express"), const express = require("express"),
router = express.Router(), router = express.Router(),
crypto = require("crypto"), crypto = require("crypto")
{ readFile, writeFile } = require("fs").promises, const { readFile, writeFile, mkdir } = require("fs").promises,
fs = require("fs"), fs = require("fs"),
accessSync = fs.accessSync, accessSync = fs.accessSync,
randomWords = require("random-words"), randomWords = require("random-words"),
@ -619,7 +619,7 @@ const loadGame = async (id) => {
return games[id]; return games[id];
} }
let game = await readFile(`games/${id}`) let game = await readFile(`/db/games/${id}`)
.catch(() => { .catch(() => {
return; return;
}); });
@ -627,20 +627,20 @@ const loadGame = async (id) => {
if (game) { if (game) {
try { try {
game = JSON.parse(game); game = JSON.parse(game);
console.log(`${info}: Creating backup of games/${id}`); console.log(`${info}: Creating backup of /db/games/${id}`);
await writeFile(`games/${id}.bk`, JSON.stringify(game)); await writeFile(`/db/games/${id}.bk`, JSON.stringify(game));
} catch (error) { } catch (error) {
console.log(`Load or parse error from games/${id}:`, error); console.log(`Load or parse error from /db/games/${id}:`, error);
console.log(`Attempting to load backup from games/${id}.bk`); console.log(`Attempting to load backup from /db/games/${id}.bk`);
game = await readFile(`games/${id}.bk`) game = await readFile(`/db/games/${id}.bk`)
.catch(() => { .catch(() => {
console.error(error, game); console.error(error, game);
}); });
if (game) { if (game) {
try { try {
game = JSON.parse(game); game = JSON.parse(game);
console.log(`Saving backup to games/${id}`); console.log(`Saving backup to /db/games/${id}`);
await writeFile(`games/${id}`, JSON.stringify(game, null, 2)); await writeFile(`/db/games/${id}`, JSON.stringify(game, null, 2));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
game = null; game = null;
@ -3448,15 +3448,16 @@ const saveGame = async (game) => {
/* Save per turn while debugging... */ /* Save per turn while debugging... */
game.step = game.step ? game.step : 0; game.step = game.step ? game.step : 0;
/* /*
await writeFile(`games/${game.id}.${game.step++}`, JSON.stringify(reducedGame, null, 2)) await writeFile(`/db/games/${game.id}.${game.step++}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => { .catch((error) => {
console.error(`${session.id} Unable to write to games/${game.id}`); console.error(`${session.id} Unable to write to /db/games/${game.id}`);
console.error(error); console.error(error);
}); });
*/ */
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2)) await mkdir('/db/games', { recursive: true });
await writeFile(`/db/games/${game.id}`, JSON.stringify(reducedGame, null, 2))
.catch((error) => { .catch((error) => {
console.error(`${session.id} Unable to write to games/${game.id}`); console.error(`Unable to write to /db/games/${game.id}`);
console.error(error); console.error(error);
}); });
} }
@ -3886,7 +3887,7 @@ router.ws("/ws/:id", async (ws, req) => {
/* Check for a game in the Winner state with no more connections /* Check for a game in the Winner state with no more connections
* and remove it */ * and remove it */
if (game.state === 'winner' || game.state === 'lobby') { if (game.state === 'winner') {
let dead = true; let dead = true;
for (let id in game.sessions) { for (let id in game.sessions) {
if (game.sessions[id].live && game.sessions[id].name) { if (game.sessions[id].live && game.sessions[id].name) {
@ -3910,9 +3911,9 @@ router.ws("/ws/:id", async (ws, req) => {
delete audio[id]; delete audio[id];
delete games[id]; delete games[id];
try { try {
fs.unlinkSync(`games/${id}`); fs.unlinkSync(`/db/games/${id}`);
} catch (error) { } catch (error) {
console.error(`${session.id}: Unable to remove games/${id}`); console.error(`${session.id}: Unable to remove /db/games/${id}`);
} }
} }
} }
@ -4655,7 +4656,7 @@ const createGame = (id) => {
id = randomWords(4).join('-'); id = randomWords(4).join('-');
try { try {
/* If file can be read, it already exists so look for a new name */ /* If file can be read, it already exists so look for a new name */
accessSync(`games/${id}`, fs.F_OK); accessSync(`/db/games/${id}`, fs.F_OK);
id = ''; id = '';
} catch (error) { } catch (error) {
break; break;

116
server/src/app.ts Normal file
View File

@ -0,0 +1,116 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import type { Request, Response, NextFunction } from 'express';
process.env.TZ = "Etc/GMT";
console.log("Loading ketr.ketran");
const express = require("express");
const bodyParser = require("body-parser");
const config = require("config");
const session = require('express-session');
const basePath = require("../basepath");
const cookieParser = require("cookie-parser");
const fs = require('fs');
const app = express();
const server = require("http").createServer(app);
app.use(cookieParser());
const ws = require('express-ws')(app, server);
require("../console-line.js"); /* Monkey-patch console.log with line numbers */
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/",
serverConfig = config.get("server");
console.log("Hosting server from: " + basePath);
let userDB: any, gameDB: any;
app.use(bodyParser.json());
/* App is behind an nginx proxy which we trust, so use the remote address
* set in the headers */
app.set("trust proxy", true);
app.set("basePath", basePath);
app.use(basePath, require("../routes/basepath.js"));
/* Handle static files first so excessive logging doesn't occur */
app.use(basePath, express.static(frontendPath, { index: false }));
const index = require("../routes/index");
if (config.has("admin")) {
const admin = config.get("admin");
app.set("admin", admin);
}
/* Allow loading of the app w/out being logged in */
app.use(basePath, index);
/* /games loads the default index */
app.use(basePath + "games", index);
app.use(function(err: any, req: Request, res: Response, next: NextFunction) {
console.error(err.message);
res.status(err.status || 500).json({
message: err.message,
error: {}
});
});
app.use(`${basePath}api/v1/games`, require("../routes/games"));
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
app.use(basePath, index);
/**
* Create HTTP server and listen for new connections
*/
app.set("port", serverConfig.port);
process.on('SIGINT', () => {
console.log("Gracefully shutting down from SIGINT (Ctrl-C) in 2 seconds");
setTimeout(() => process.exit(-1), 2000);
server.close(() => process.exit(1));
});
require("../db/games").then(function(db: any) {
gameDB = db;
}).then(function() {
return require("../db/users").then(function(db: any) {
userDB = db;
});
}).then(function() {
console.log("DB connected. Opening server.");
server.listen(serverConfig.port, () => {
console.log(`http/ws server listening on ${serverConfig.port}`);
});
}).catch(function(error: any) {
console.error(error);
process.exit(-1);
});
server.on("error", function(error: any) {
if (error.syscall !== "listen") {
throw error;
}
// handle specific listen errors with friendly messages
switch (error.code) {
case "EACCES":
console.error(serverConfig.port + " requires elevated privileges");
process.exit(1);
break;
case "EADDRINUSE":
console.error(serverConfig.port + " is already in use");
process.exit(1);
break;
default:
throw error;
}
});
module.exports = { app, server };

11
server/tests/app.test.ts Normal file
View File

@ -0,0 +1,11 @@
import request from 'supertest';
const { app } = require('../src/app');
describe('Server Routes', () => {
it('should respond to GET /', async () => {
const response = await request(app).get('/');
expect(response.status).toBe(200);
});
// Add more tests as needed
});

19
server/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"strict": false,
"noImplicitAny": false,
"moduleResolution": "Node",
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}