feat: custom server that works

This commit is contained in:
Derock 2023-11-10 20:35:55 -05:00
parent 2e86e827ca
commit e151a91290
No known key found for this signature in database
17 changed files with 1372 additions and 284 deletions

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ next-env.d.ts
# production # production
/build /build
/dist
# misc # misc
.DS_Store .DS_Store

View file

@ -1,10 +1,9 @@
import type { Config } from "drizzle-kit"; import type { Config } from "drizzle-kit";
import { env } from "~/env.mjs";
export default { export default {
schema: "./src/server/db/schema.ts", schema: "./src/server/db/schema.ts",
driver: "better-sqlite", driver: "better-sqlite",
dbCredentials: { dbCredentials: {
url: env.DATABASE_PATH, url: process.env.DATABASE_PATH ?? "./data/db.sqlite",
}, },
} satisfies Config; } satisfies Config;

View file

@ -1,9 +1,3 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.mjs");
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
experimental: { experimental: {

View file

@ -2,15 +2,20 @@
"name": "hostforge", "name": "hostforge",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"build": "next build", "build:next": "next build",
"build:server-tsc": "tsc -p tsconfig.server.json && tscpaths -p tsconfig.server.json -s ./src -o ./dist/",
"build:server": "tsup",
"clean": "rm -rf .next dist",
"db:push": "drizzle-kit push:sqlite", "db:push": "drizzle-kit push:sqlite",
"dev": "tsx watch src/server/server.ts", "dev": "npm-run-all build:server dev:run",
"dev:run": "node --enable-source-maps dist/server.js",
"lint": "next lint", "lint": "next lint",
"start": "next start" "start": "node -r tsconfig"
}, },
"dependencies": { "dependencies": {
"@mantine/form": "^7.1.7", "@mantine/form": "^7.2.1",
"@prisma/migrate": "^5.5.2", "@prisma/migrate": "^5.5.2",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
@ -32,14 +37,15 @@
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"drizzle-orm": "^0.28.6", "drizzle-orm": "^0.28.6",
"extensionless": "^1.7.3",
"ipaddr.js": "^2.1.0", "ipaddr.js": "^2.1.0",
"next": "^14.0.1", "next": "14.0.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-os-utils": "^1.3.7", "node-os-utils": "^1.3.7",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"recharts": "^2.9.2", "recharts": "^2.9.3",
"sonner": "^1.2.0", "sonner": "^1.2.0",
"superjson": "^2.2.0", "superjson": "^2.2.0",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
@ -51,26 +57,31 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.6", "@types/better-sqlite3": "^7.6.7",
"@types/bunyan": "^1.8.10", "@types/bunyan": "^1.8.11",
"@types/bunyan-format": "^0.2.7", "@types/bunyan-format": "^0.2.8",
"@types/eslint": "^8.44.6", "@types/eslint": "^8.44.7",
"@types/node": "^20.8.10", "@types/node": "^20.9.0",
"@types/node-os-utils": "^1.3.4", "@types/node-os-utils": "^1.3.4",
"@types/react": "^18.2.35", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.15",
"@types/ua-parser-js": "^0.7.38", "@types/ua-parser-js": "^0.7.39",
"@types/ws": "^8.5.9", "@types/ws": "^8.5.9",
"@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.9.1", "@typescript-eslint/parser": "^6.10.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"drizzle-kit": "^0.19.13", "drizzle-kit": "^0.19.13",
"eslint": "^8.53.0", "eslint": "^8.53.0",
"eslint-config-next": "^14.0.1", "eslint-config-next": "^14.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6", "prettier-plugin-tailwindcss": "^0.5.6",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"tscpaths": "^0.0.9",
"tsup": "^7.2.0",
"tsx": "^3.14.0", "tsx": "^3.14.0",
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { type NextRequest } from "next/server"; import { type NextRequest } from "next/server.js";
import { env } from "~/env.mjs"; import { env } from "~/env";
import { appRouter } from "~/server/api/root"; import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc"; import { createTRPCContext } from "~/server/api/trpc";

View file

@ -1,11 +1,10 @@
"use client"; "use client";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { RiPulseFill } from "react-icons/ri";
import { ResponsiveContainer, AreaChart, Area } from "recharts"; import { ResponsiveContainer, AreaChart, Area } from "recharts";
import styles from "./StatCard.module.css"; // import styles from "./StatCard.module.css";
export function StatCard<T extends { [key: string]: number }>(props: { export function StatCard<T extends Record<string, number>>(props: {
title: string; title: string;
value: string; value: string;
subvalue: string; subvalue: string;
@ -73,7 +72,7 @@ export function StatCard<T extends { [key: string]: number }>(props: {
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className={`relative z-10 ${styles["stat-card"]}`}> <div className={`relative z-10 ${/*styles["stat-card"]*/ "asdf"}`}>
<p className="stroke stroke-card text-2xl font-bold">{props.value}</p> <p className="stroke stroke-card text-2xl font-bold">{props.value}</p>
<p className="stroke stroke-card text-sm text-muted-foreground"> <p className="stroke stroke-card text-sm text-muted-foreground">
{props.subvalue} {props.subvalue}

View file

@ -7,6 +7,7 @@ export const env = createEnv({
* isn't built with invalid env vars. * isn't built with invalid env vars.
*/ */
server: { server: {
// make sure to update drizzle.config.js if you change this
DATABASE_PATH: z.string().default("./data/db.sqlite"), DATABASE_PATH: z.string().default("./data/db.sqlite"),
NODE_ENV: z NODE_ENV: z
@ -16,7 +17,10 @@ export const env = createEnv({
SQLITE_UUIDV7_EXT_PATH: z.string().optional(), SQLITE_UUIDV7_EXT_PATH: z.string().optional(),
SESSION_SECRET: z.string().min(8), SESSION_SECRET: z.string().min(8),
HOSTNAME: z.string().default("localhost"), HOSTNAME: z.string().default("localhost"),
PORT: z.number().default(3000), PORT: z
.string()
.default("3000")
.transform((str) => parseInt(str)),
}, },
/** /**

View file

@ -1,6 +1,7 @@
import pacakge from "../package.json"; // import pacakge from "../package.json";
import { env } from "./env.mjs"; import { env } from "~/env";
const { version } = pacakge; // const { version } = pacakge;
const version = "1.0.0";
export async function register() { export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") { if (process.env.NEXT_RUNTIME === "nodejs") {
@ -8,15 +9,17 @@ export async function register() {
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator"); const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
const { db } = await import("./server/db"); const { db } = await import("./server/db");
const { mkdir, stat } = await import("fs/promises"); const { mkdir, stat } = await import("fs/promises");
const { dirname } = await import("path"); const path = await import("path");
// check if database folder exists // check if database folder exists
try { try {
const dir = dirname(env.DATABASE_PATH); const dir = path.dirname(env.DATABASE_PATH);
await stat(dir); await stat(dir);
} catch (e) { } catch (e) {
await mkdir(dirname(env.DATABASE_PATH), { recursive: true }); await mkdir(path.dirname(env.DATABASE_PATH), { recursive: true });
logger.debug(`Created database folder ${dirname(env.DATABASE_PATH)}`); logger.debug(
`Created database folder ${path.dirname(env.DATABASE_PATH)}`,
);
} }
if (env.NODE_ENV === "production") { if (env.NODE_ENV === "production") {

View file

@ -7,7 +7,7 @@
* need to use are documented accordingly near the end. * need to use are documented accordingly near the end.
*/ */
import { TRPCError, initTRPC } from "@trpc/server"; import { TRPCError, initTRPC } from "@trpc/server";
import { type NextRequest } from "next/server"; import { type NextRequest } from "next/server.js";
import superjson from "superjson"; import superjson from "superjson";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { db } from "~/server/db"; import { db } from "~/server/db";

View file

@ -3,9 +3,9 @@ import { db } from "../db";
import { users, sessions } from "../db/schema"; import { users, sessions } from "../db/schema";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import assert from "assert"; import assert from "assert";
import { NextRequest, userAgent } from "next/server"; import { NextRequest, userAgent } from "next/server.js";
import { hash as argon2Hash } from "argon2"; import { hash as argon2Hash } from "argon2";
import { env } from "~/env.mjs"; import { env } from "~/env";
export type SessionUpdateData = Partial<{ export type SessionUpdateData = Partial<{
ua: string; ua: string;

View file

@ -1,7 +1,7 @@
import { drizzle } from "drizzle-orm/better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3";
import SQLite3 from "better-sqlite3"; import SQLite3 from "better-sqlite3";
import { join } from "path"; import { join } from "path";
import { env } from "~/env.mjs"; import { env } from "~/env";
const sqlite = new SQLite3(env.DATABASE_PATH); const sqlite = new SQLite3(env.DATABASE_PATH);

View file

@ -1,10 +1,12 @@
// import { config } from "dotenv";
// config();
import "dotenv/config"; import "dotenv/config";
import next from "next"; import next from "next";
import { env } from "~/env.mjs"; import { env } from "~/env";
import { createServer } from "http"; import { createServer } from "http";
import logger from "./utils/logger"; import logger from "./utils/logger";
import ws from "ws"; // import ws from "ws";
import { WebSocketServer } from "ws";
import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { appRouter } from "./api/root"; import { appRouter } from "./api/root";
import { createTRPCContext } from "./api/trpc"; import { createTRPCContext } from "./api/trpc";
@ -16,6 +18,9 @@ async function startApp() {
dev: env.NODE_ENV !== "production", dev: env.NODE_ENV !== "production",
hostname: env.HOSTNAME, hostname: env.HOSTNAME,
port: env.PORT, port: env.PORT,
// dir: path.join(__dirname, "../.."),
customServer: true,
isNodeDebugging: true,
}); });
await app.prepare(); await app.prepare();
@ -26,18 +31,15 @@ async function startApp() {
// create the http server // create the http server
const server = createServer((req, res) => { const server = createServer((req, res) => {
try { getHandler(req, res).catch((error) => {
// handle the request
getHandler(req, res);
} catch (error) {
logger.error(error); logger.error(error);
res.statusCode = 500; res.statusCode = 500;
res.end("Internal Server Error"); res.end("Internal Server Error");
} });
}); });
// create the websocket server // create the websocket server
const wss = new ws.Server({ noServer: true }); const wss = new WebSocketServer({ noServer: true });
const trpcHandler = applyWSSHandler({ const trpcHandler = applyWSSHandler({
wss, wss,
router: appRouter, router: appRouter,
@ -49,6 +51,7 @@ async function startApp() {
}); });
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
logger.warn("SIGTERM received, shutting down...");
trpcHandler.broadcastReconnectNotification(); trpcHandler.broadcastReconnectNotification();
server.close(() => { server.close(() => {
process.exit(0); process.exit(0);
@ -59,9 +62,9 @@ async function startApp() {
server.on("upgrade", (req, socket, head) => { server.on("upgrade", (req, socket, head) => {
// send trpc requests to the trpc server // send trpc requests to the trpc server
if (req.url?.startsWith("/api/trpc")) { if (req.url?.startsWith("/api/trpc")) {
wss.handleUpgrade(req, socket, head, () => {}); wss.handleUpgrade(req, socket, head, () => undefined);
} else { } else {
upgradeHandler(req, socket, head); void upgradeHandler(req, socket, head);
} }
}); });
@ -71,4 +74,4 @@ async function startApp() {
}); });
} }
startApp(); void startApp();

View file

@ -1,6 +1,6 @@
import assert from "assert"; import assert from "assert";
import { IncomingMessage } from "http"; import { type IncomingMessage } from "http";
import { NextRequest } from "next/server"; import { NextRequest } from "next/server.js";
/** /**
* Turns an node:http IncomingMessage into a next.js request * Turns an node:http IncomingMessage into a next.js request

View file

@ -1,9 +1,20 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "ES2020",
"outDir": "dist", "outDir": "dist",
"noEmit": false "moduleResolution": "node",
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"isolatedModules": false,
"noEmit": false,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
},
"allowSyntheticDefaultImports": true,
"sourceMap": true
}, },
"include": ["./src/**/*"] "include": ["next-env.d.ts", "src/server/**/*.ts", "src/server/**/*.tsx"]
} }

17
tsup.config.ts Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig, type Options } from "tsup";
const opts: Options = {
platform: "node",
format: ["esm"],
treeshake: true,
clean: true,
sourcemap: true,
tsconfig: "tsconfig.server.json",
};
export default defineConfig([
{
entryPoints: ["src/server/server.ts"],
...opts,
},
]);