initial commit

This commit is contained in:
creations 2025-05-01 13:53:38 -04:00
commit 44077f3ccd
Signed by: creations
GPG key ID: 8F553AA4320FC711
14 changed files with 461 additions and 0 deletions

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# bun frontend template
a simple bun frontend starting point i made and use

35
biome.json Normal file
View file

@ -0,0 +1,35 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": true,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineEnding": "lf"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"indentStyle": "tab",
"lineEnding": "lf",
"jsxQuoteStyle": "double",
"semicolons": "always"
}
}
}

6
config/environment.ts Normal file
View file

@ -0,0 +1,6 @@
export const environment: Environment = {
port: Number.parseInt(process.env.PORT || "8080", 10),
host: process.env.HOST || "0.0.0.0",
development:
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
};

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "void_backend",
"module": "src/index.ts",
"type": "module",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run --hot src/index.ts --dev",
"lint": "bunx biome check",
"lint:fix": "bunx biome check --fix",
"cleanup": "rm -rf logs node_modules bun.lockdb"
},
"devDependencies": {
"@types/bun": "^1.2.10",
"@types/ejs": "^3.1.5",
"globals": "^16.0.0",
"@biomejs/biome": "^1.9.4"
},
"peerDependencies": {
"typescript": "^5.8.2"
},
"dependencies": {
"@creations.works/logger": "^1.0.3"
}
}

BIN
public/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

12
src/index.ts Normal file
View file

@ -0,0 +1,12 @@
import { logger } from "@creations.works/logger";
import { serverHandler } from "@/server";
async function main(): Promise<void> {
serverHandler.initialize();
}
main().catch((error: Error) => {
logger.error(["Error initializing the server:", error]);
process.exit(1);
});

22
src/routes/index.ts Normal file
View file

@ -0,0 +1,22 @@
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "application/json",
};
async function handler(request: ExtendedRequest): Promise<Response> {
const endPerf: number = Date.now();
const perf: number = endPerf - request.startPerf;
const { query, params } = request;
const response: Record<string, unknown> = {
perf,
query,
params,
};
return Response.json(response);
}
export { handler, routeDef };

253
src/server.ts Normal file
View file

@ -0,0 +1,253 @@
import { resolve } from "node:path";
import { environment } from "@config/environment";
import { logger } from "@creations.works/logger";
import {
type BunFile,
FileSystemRouter,
type MatchedRoute,
type Serve,
} from "bun";
import { webSocketHandler } from "@/websocket";
class ServerHandler {
private router: FileSystemRouter;
constructor(
private port: number,
private host: string,
) {
this.router = new FileSystemRouter({
style: "nextjs",
dir: "./src/routes",
fileExtensions: [".ts"],
origin: `http://${this.host}:${this.port}`,
});
}
public initialize(): void {
const server: Serve = Bun.serve({
port: this.port,
hostname: this.host,
fetch: this.handleRequest.bind(this),
websocket: {
open: webSocketHandler.handleOpen.bind(webSocketHandler),
message: webSocketHandler.handleMessage.bind(webSocketHandler),
close: webSocketHandler.handleClose.bind(webSocketHandler),
},
});
logger.info(`Server running at http://${server.hostname}:${server.port}`, {
breakLine: true,
});
this.logRoutes();
}
private logRoutes(): void {
logger.info("Available routes:");
const sortedRoutes: [string, string][] = Object.entries(
this.router.routes,
).sort(([pathA]: [string, string], [pathB]: [string, string]) =>
pathA.localeCompare(pathB),
);
for (const [path, filePath] of sortedRoutes) {
logger.info(`Route: ${path}, File: ${filePath}`);
}
}
private async serveStaticFile(pathname: string): Promise<Response> {
try {
let filePath: string;
if (pathname === "/favicon.ico") {
filePath = resolve("public", "assets", "favicon.ico");
} else {
filePath = resolve(`.${pathname}`);
}
const file: BunFile = Bun.file(filePath);
if (await file.exists()) {
const fileContent: ArrayBuffer = await file.arrayBuffer();
const contentType: string = file.type || "application/octet-stream";
return new Response(fileContent, {
headers: { "Content-Type": contentType },
});
}
logger.warn(`File not found: ${filePath}`);
return new Response("Not Found", { status: 404 });
} catch (error) {
logger.error([`Error serving static file: ${pathname}`, error as Error]);
return new Response("Internal Server Error", { status: 500 });
}
}
private async handleRequest(
request: Request,
server: BunServer,
): Promise<Response> {
const extendedRequest: ExtendedRequest = request as ExtendedRequest;
extendedRequest.startPerf = performance.now();
const pathname: string = new URL(request.url).pathname;
if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
return await this.serveStaticFile(pathname);
}
const match: MatchedRoute | null = this.router.match(request);
let requestBody: unknown = {};
let response: Response;
if (match) {
const { filePath, params, query } = match;
try {
const routeModule: RouteModule = await import(filePath);
const contentType: string | null = request.headers.get("Content-Type");
const actualContentType: string | null = contentType
? contentType.split(";")[0].trim()
: null;
if (
routeModule.routeDef.needsBody === "json" &&
actualContentType === "application/json"
) {
try {
requestBody = await request.json();
} catch {
requestBody = {};
}
} else if (
routeModule.routeDef.needsBody === "multipart" &&
actualContentType === "multipart/form-data"
) {
try {
requestBody = await request.formData();
} catch {
requestBody = {};
}
}
if (
(Array.isArray(routeModule.routeDef.method) &&
!routeModule.routeDef.method.includes(request.method)) ||
(!Array.isArray(routeModule.routeDef.method) &&
routeModule.routeDef.method !== request.method)
) {
response = Response.json(
{
success: false,
code: 405,
error: `Method ${request.method} Not Allowed, expected ${
Array.isArray(routeModule.routeDef.method)
? routeModule.routeDef.method.join(", ")
: routeModule.routeDef.method
}`,
},
{ status: 405 },
);
} else {
const expectedContentType: string | string[] | null =
routeModule.routeDef.accepts;
let matchesAccepts: boolean;
if (Array.isArray(expectedContentType)) {
matchesAccepts =
expectedContentType.includes("*/*") ||
expectedContentType.includes(actualContentType || "");
} else {
matchesAccepts =
expectedContentType === "*/*" ||
actualContentType === expectedContentType;
}
if (!matchesAccepts) {
response = Response.json(
{
success: false,
code: 406,
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
Array.isArray(expectedContentType)
? expectedContentType.join(", ")
: expectedContentType
}`,
},
{ status: 406 },
);
} else {
extendedRequest.params = params;
extendedRequest.query = query;
response = await routeModule.handler(
extendedRequest,
requestBody,
server,
);
if (routeModule.routeDef.returns !== "*/*") {
response.headers.set(
"Content-Type",
routeModule.routeDef.returns,
);
}
}
}
} catch (error: unknown) {
logger.error([`Error handling route ${request.url}:`, error as Error]);
response = Response.json(
{
success: false,
code: 500,
error: "Internal Server Error",
},
{ status: 500 },
);
}
} else {
response = Response.json(
{
success: false,
code: 404,
error: "Not Found",
},
{ status: 404 },
);
}
const headers = request.headers;
let ip = server.requestIP(request)?.address;
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
ip =
headers.get("CF-Connecting-IP")?.trim() ||
headers.get("X-Real-IP")?.trim() ||
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
"unknown";
}
logger.custom(
`[${request.method}]`,
`(${response.status})`,
[
request.url,
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
ip || "unknown",
],
"90",
);
return response;
}
}
const serverHandler: ServerHandler = new ServerHandler(
environment.port,
environment.host,
);
export { serverHandler };

30
src/websocket.ts Normal file
View file

@ -0,0 +1,30 @@
import { logger } from "@creations.works/logger";
import type { ServerWebSocket } from "bun";
class WebSocketHandler {
public handleMessage(ws: ServerWebSocket, message: string): void {
logger.info(`WebSocket received: ${message}`);
try {
ws.send(`You said: ${message}`);
} catch (error) {
logger.error(["WebSocket send error", error as Error]);
}
}
public handleOpen(ws: ServerWebSocket): void {
logger.info("WebSocket connection opened.");
try {
ws.send("Welcome to the WebSocket server!");
} catch (error) {
logger.error(["WebSocket send error", error as Error]);
}
}
public handleClose(ws: ServerWebSocket, code: number, reason: string): void {
logger.warn(`WebSocket closed with code ${code}, reason: ${reason}`);
}
}
const webSocketHandler: WebSocketHandler = new WebSocketHandler();
export { webSocketHandler };

33
tsconfig.json Normal file
View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@config/*": ["config/*"],
"@types/*": ["types/*"],
"@helpers/*": ["src/helpers/*"]
},
"typeRoots": ["./src/types", "./node_modules/@types"],
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": ["src", "types", "config"]
}

14
types/bun.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
import type { Server } from "bun";
type Query = Record<string, string>;
type Params = Record<string, string>;
declare global {
type BunServer = Server;
interface ExtendedRequest extends Request {
startPerf: number;
query: Query;
params: Params;
}
}

5
types/config.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
type Environment = {
port: number;
host: string;
development: boolean;
};

9
types/logger.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
type ILogMessagePart = { value: string; color: string };
type ILogMessageParts = {
level: ILogMessagePart;
filename: ILogMessagePart;
readableTimestamp: ILogMessagePart;
message: ILogMessagePart;
[key: string]: ILogMessagePart;
};

15
types/routes.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
type RouteDef = {
method: string | string[];
accepts: string | null | string[];
returns: string;
needsBody?: "multipart" | "json";
};
type RouteModule = {
handler: (
request: Request | ExtendedRequest,
requestBody: unknown,
server: BunServer,
) => Promise<Response> | Response;
routeDef: RouteDef;
};