initial commit
This commit is contained in:
commit
44077f3ccd
14 changed files with 461 additions and 0 deletions
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# bun frontend template
|
||||
|
||||
a simple bun frontend starting point i made and use
|
35
biome.json
Normal file
35
biome.json
Normal 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
6
config/environment.ts
Normal 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
24
package.json
Normal 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
BIN
public/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
12
src/index.ts
Normal file
12
src/index.ts
Normal 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
22
src/routes/index.ts
Normal 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
253
src/server.ts
Normal 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
30
src/websocket.ts
Normal 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
33
tsconfig.json
Normal 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
14
types/bun.d.ts
vendored
Normal 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
5
types/config.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
type Environment = {
|
||||
port: number;
|
||||
host: string;
|
||||
development: boolean;
|
||||
};
|
9
types/logger.d.ts
vendored
Normal file
9
types/logger.d.ts
vendored
Normal 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
15
types/routes.d.ts
vendored
Normal 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;
|
||||
};
|
Loading…
Add table
Reference in a new issue