import ejs from "ejs"; import Fastify from "fastify"; import { readdir, stat } from "fs/promises"; import { IncomingMessage, Server, ServerResponse } from "http"; import { join } from "path"; import { environment } from "../../config/environment"; // types import type { FastifyInstance, FastifyRequest } from "fastify"; import type { Stats } from "fs"; import type { AddressInfo } from "net"; // official plugins import { fastifyStatic } from "@fastify/static"; import { fastifyView } from "@fastify/view"; import fastifyWebSocket from '@fastify/websocket'; //custom plugins import faviconPlugin from "./plugins/favicon"; class FastifyManager { private readonly port: number = environment.fastify.port; private readonly server: FastifyInstance; constructor() { this.server = Fastify({ logger: false, trustProxy: !environment.development, ignoreTrailingSlash: true }); } private async registerPlugins(): Promise { // official plugins this.server.register(fastifyView, { engine: { ejs }, root: environment.paths.www.views, includeViewExtension: true }); this.server.register(fastifyStatic, { root: environment.paths.www.public, prefix: "/public/", prefixAvoidTrailingSlash: true }); await this.server.register(fastifyWebSocket, { options: { maxPayload: 1048576 } }); // custom plugins this.server.register(faviconPlugin, { path: join(environment.paths.www.public, "assets", "favicon.gif") }); } // dynamic route loading private async loadRoutes(): Promise { const routePaths: [string, string, boolean][] = [ ["routes", "/", false], ["../api", "/api", true] ]; for (const [routePath, prefix, recursive] of routePaths) { const modifiedRoutePath: string = join(environment.paths.www.root, routePath); let files: string[]; try { files = await this.readDirRecursive(modifiedRoutePath, recursive); } catch (err) { console.error("Failed to read route directory", modifiedRoutePath, "error:", err); return; } for (const file of files) { try { const routeModule = await import(file); const { default: routeData } = routeModule; if (!routeData || !routeData.routeInfo || !routeData.route) { console.error(`Failed to load route from ${file}:`, "Route data is missing"); continue; } if (routeData.routeInfo.enabled && routeData.route) { const { routeInfo, route } = routeData; let routePath = routeInfo.path || "/"; if (prefix) { routePath = routePath === "/" ? prefix : join(prefix, routePath); } routePath = routePath.replace(/\\/g, '/'); if (!routePath.startsWith('/')) { routePath = '/' + routePath; } routePath = routePath.replace(/\/+/g, "/"); const methods = Array.isArray(routeInfo.method) ? routeInfo.method : [routeInfo.method]; for (const method of methods) { if (routeInfo.websocket) { this.server.get(routePath, { websocket: true }, function wsHandler(socket: any, req: FastifyRequest) { route(socket, req); }); continue; } else { this.server.route({ method, url: routePath, handler: route }); } } } } catch (err) { console.error(`Failed to load route from ${file}:`, err); } } } } private async readDirRecursive(dir: string, recursive: boolean): Promise { let results: string[] = []; const list: string[] = await readdir(dir); for (const file of list) { const filePath: string = join(dir, file); const statObj: Stats = await stat(filePath); if (statObj && statObj.isDirectory()) { if (recursive) { results = results.concat(await this.readDirRecursive(filePath, recursive)); } } else { results.push(filePath); } } return results; } public async start(): Promise { try { await this.registerPlugins(); await this.loadRoutes(); await this.server.listen({ port: environment.fastify.port, host: environment.fastify.host }); const [_address, _port, scheme]: [string, number, string] = ((): [string, number, string] => { const address: string | AddressInfo | null = this.server.server.address(); const resolvedAddress: [string, number] = (address && typeof address === 'object') ? [(address.address.startsWith("::") || address.address === "0.0.0.0") ? "localhost" : address.address, address.port] : ["localhost", this.port]; const resolvedScheme: string = resolvedAddress[0] === "localhost" ? "http" : "https"; return [...resolvedAddress, resolvedScheme]; })(); console.info("Started Listening on", `${scheme}://${_address}:${_port}`); console.info("Registered routes: ", "\n", this.server.printRoutes()); } catch (error) { console.error("Failed to start Fastify server", error); } } } const fastifyManager = new FastifyManager(); export default fastifyManager; export { fastifyManager };