Migrate to @atums/echo logger and improve project structure
Some checks failed
Code quality checks / biome (push) Failing after 12s
Some checks failed
Code quality checks / biome (push) Failing after 12s
This commit is contained in:
parent
490cd9d0e0
commit
fdce8810cf
11 changed files with 276 additions and 100 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
bun.lock
|
bun.lock
|
||||||
robots.txt
|
robots.txt
|
||||||
|
logs
|
||||||
|
public/custom
|
||||||
|
|
28
LICENSE
Normal file
28
LICENSE
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2025, creations.works
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
68
README.md
68
README.md
|
@ -1,3 +1,67 @@
|
||||||
# bun frontend template
|
# Bun Frontend Template
|
||||||
|
|
||||||
a simple bun frontend starting point i made and use
|
A minimal, fast, and type-safe web server template built with [Bun](https://bun.sh) and TypeScript. Features file-system based routing, static file serving, WebSocket support, and structured logging.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `HOST` | Server host address | `0.0.0.0` | ✅ |
|
||||||
|
| `PORT` | Server port | `8080` | ✅ |
|
||||||
|
| `NODE_ENV` | Environment mode | `production` | ❌ |
|
||||||
|
|
||||||
|
### Creating Routes
|
||||||
|
|
||||||
|
Routes are automatically generated from files in `src/routes/`. Each route file exports:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/example.ts
|
||||||
|
const routeDef: RouteDef = {
|
||||||
|
method: "GET", // HTTP method(s)
|
||||||
|
accepts: "application/json", // Content-Type validation
|
||||||
|
returns: "application/json", // Response Content-Type
|
||||||
|
needsBody?: "json" | "multipart" // Optional body parsing, dont include if neither are required
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
request: ExtendedRequest,
|
||||||
|
requestBody: unknown,
|
||||||
|
server: BunServer
|
||||||
|
): Promise<Response> {
|
||||||
|
return Response.json({ message: "Hello World" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, routeDef };
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Features
|
||||||
|
|
||||||
|
- **Method Validation** - Automatic HTTP method checking
|
||||||
|
- **Content-Type Validation** - Request/response content type enforcement
|
||||||
|
- **Body Parsing** - Automatic JSON/FormData parsing
|
||||||
|
- **Query Parameters** - Automatic query string parsing
|
||||||
|
- **URL Parameters** - Next.js-style dynamic routes (`[id].ts`)
|
||||||
|
|
||||||
|
## Static Files
|
||||||
|
|
||||||
|
Place files in `public/` directory
|
||||||
|
|
||||||
|
### Custom Public Files
|
||||||
|
|
||||||
|
Files in `public/custom/` are served with security checks:
|
||||||
|
- Path traversal protection
|
||||||
|
- Content-type detection
|
||||||
|
- Direct file serving
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the BSD-3-Clause - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **[@atums/echo](https://www.npmjs.com/package/@atums/echo)** - Structured logging with daily rotation
|
||||||
|
- **[Bun](https://bun.sh)** - Fast JavaScript runtime and bundler
|
||||||
|
- **[TypeScript](https://www.typescriptlang.org/)** - Type-safe JavaScript
|
||||||
|
- **[Biome](https://biomejs.dev/)** - Fast formatter and linter
|
||||||
|
|
24
biome.json
24
biome.json
|
@ -7,7 +7,7 @@
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": true,
|
"ignoreUnknown": true,
|
||||||
"ignore": []
|
"ignore": ["dist"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -17,11 +17,29 @@
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
"css": {
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "tab",
|
||||||
|
"lineEnding": "lf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
}
|
"correctness": {
|
||||||
|
"noUnusedImports": "error",
|
||||||
|
"noUnusedVariables": "error"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noConsole": "error"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useConst": "error",
|
||||||
|
"noVar": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignore": ["types"]
|
||||||
},
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { resolve } from "node:path";
|
import { echo } from "@atums/echo";
|
||||||
import { logger } from "@creations.works/logger";
|
|
||||||
|
|
||||||
const environment: Environment = {
|
const environment: Environment = {
|
||||||
port: Number.parseInt(process.env.PORT || "8080", 10),
|
port: Number.parseInt(process.env.PORT || "8080", 10),
|
||||||
|
@ -8,10 +7,6 @@ const environment: Environment = {
|
||||||
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const robotstxtPath: string | null = process.env.ROBOTS_FILE
|
|
||||||
? resolve(process.env.ROBOTS_FILE)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
function verifyRequiredVariables(): void {
|
function verifyRequiredVariables(): void {
|
||||||
const requiredVariables = ["HOST", "PORT"];
|
const requiredVariables = ["HOST", "PORT"];
|
||||||
|
|
||||||
|
@ -20,7 +15,7 @@ function verifyRequiredVariables(): void {
|
||||||
for (const key of requiredVariables) {
|
for (const key of requiredVariables) {
|
||||||
const value = process.env[key];
|
const value = process.env[key];
|
||||||
if (value === undefined || value.trim() === "") {
|
if (value === undefined || value.trim() === "") {
|
||||||
logger.error(`Missing or empty environment variable: ${key}`);
|
echo.error(`Missing or empty environment variable: ${key}`);
|
||||||
hasError = true;
|
hasError = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,4 +25,4 @@ function verifyRequiredVariables(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { environment, robotstxtPath, verifyRequiredVariables };
|
export { environment, verifyRequiredVariables };
|
||||||
|
|
39
logger.json
Normal file
39
logger.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"directory": "logs",
|
||||||
|
"level": "debug",
|
||||||
|
"disableFile": false,
|
||||||
|
|
||||||
|
"rotate": true,
|
||||||
|
"maxFiles": 3,
|
||||||
|
|
||||||
|
"console": true,
|
||||||
|
"consoleColor": true,
|
||||||
|
|
||||||
|
"dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
|
||||||
|
"timezone": "local",
|
||||||
|
|
||||||
|
"silent": false,
|
||||||
|
|
||||||
|
"pattern": "{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{color:blue}{line}{reset}:{color:blue}{column}{color:gray}){reset} {data}",
|
||||||
|
"levelColor": {
|
||||||
|
"debug": "blue",
|
||||||
|
"info": "green",
|
||||||
|
"warn": "yellow",
|
||||||
|
"error": "red",
|
||||||
|
"fatal": "red"
|
||||||
|
},
|
||||||
|
|
||||||
|
"customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
|
||||||
|
"customColors": {
|
||||||
|
"GET": "green",
|
||||||
|
"POST": "blue",
|
||||||
|
"PUT": "yellow",
|
||||||
|
"DELETE": "red",
|
||||||
|
"PATCH": "cyan",
|
||||||
|
"HEAD": "magenta",
|
||||||
|
"OPTIONS": "white",
|
||||||
|
"TRACE": "gray"
|
||||||
|
},
|
||||||
|
|
||||||
|
"prettyPrint": true
|
||||||
|
}
|
11
package.json
11
package.json
|
@ -10,16 +10,13 @@
|
||||||
"cleanup": "rm -rf logs node_modules bun.lockdb"
|
"cleanup": "rm -rf logs node_modules bun.lockdb"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.2.10",
|
"@types/bun": "latest",
|
||||||
"@types/ejs": "^3.1.5",
|
"@biomejs/biome": "latest"
|
||||||
"globals": "^16.0.0",
|
|
||||||
"@biomejs/biome": "^1.9.4"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.8.2"
|
"typescript": "latest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@creations.works/logger": "^1.0.3",
|
"@atums/echo": "latest"
|
||||||
"ejs": "^3.1.10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { logger } from "@creations.works/logger";
|
import { echo } from "@atums/echo";
|
||||||
|
|
||||||
import { serverHandler } from "@/server";
|
|
||||||
import { verifyRequiredVariables } from "@config/environment";
|
import { verifyRequiredVariables } from "@config/environment";
|
||||||
|
import { serverHandler } from "@server";
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
verifyRequiredVariables();
|
verifyRequiredVariables();
|
||||||
|
@ -10,6 +10,6 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error: Error) => {
|
main().catch((error: Error) => {
|
||||||
logger.error(["Error initializing the server:", error]);
|
echo.error({ message: "Error initializing the server:", error });
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
138
src/server.ts
138
src/server.ts
|
@ -1,14 +1,14 @@
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { environment, robotstxtPath } from "@config/environment";
|
import { echo } from "@atums/echo";
|
||||||
import { logger } from "@creations.works/logger";
|
import { environment } from "@config/environment";
|
||||||
import {
|
import {
|
||||||
type BunFile,
|
type BunFile,
|
||||||
FileSystemRouter,
|
FileSystemRouter,
|
||||||
type MatchedRoute,
|
type MatchedRoute,
|
||||||
type Serve,
|
type Server,
|
||||||
} from "bun";
|
} from "bun";
|
||||||
|
|
||||||
import { webSocketHandler } from "@/websocket";
|
import { webSocketHandler } from "@websocket";
|
||||||
|
|
||||||
class ServerHandler {
|
class ServerHandler {
|
||||||
private router: FileSystemRouter;
|
private router: FileSystemRouter;
|
||||||
|
@ -19,14 +19,14 @@ class ServerHandler {
|
||||||
) {
|
) {
|
||||||
this.router = new FileSystemRouter({
|
this.router = new FileSystemRouter({
|
||||||
style: "nextjs",
|
style: "nextjs",
|
||||||
dir: "./src/routes",
|
dir: resolve("src", "routes"),
|
||||||
fileExtensions: [".ts"],
|
fileExtensions: [".ts"],
|
||||||
origin: `http://${this.host}:${this.port}`,
|
origin: `http://${this.host}:${this.port}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
const server: Serve = Bun.serve({
|
const server: Server = Bun.serve({
|
||||||
port: this.port,
|
port: this.port,
|
||||||
hostname: this.host,
|
hostname: this.host,
|
||||||
fetch: this.handleRequest.bind(this),
|
fetch: this.handleRequest.bind(this),
|
||||||
|
@ -37,15 +37,13 @@ class ServerHandler {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Server running at http://${server.hostname}:${server.port}`, {
|
echo.info(`Server running at http://${server.hostname}:${server.port}`);
|
||||||
breakLine: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logRoutes();
|
this.logRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
private logRoutes(): void {
|
private logRoutes(): void {
|
||||||
logger.info("Available routes:");
|
echo.info("Available routes:");
|
||||||
|
|
||||||
const sortedRoutes: [string, string][] = Object.entries(
|
const sortedRoutes: [string, string][] = Object.entries(
|
||||||
this.router.routes,
|
this.router.routes,
|
||||||
|
@ -54,14 +52,19 @@ class ServerHandler {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [path, filePath] of sortedRoutes) {
|
for (const [path, filePath] of sortedRoutes) {
|
||||||
logger.info(`Route: ${path}, File: ${filePath}`);
|
echo.info(`Route: ${path}, File: ${filePath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async serveStaticFile(pathname: string): Promise<Response> {
|
private async serveStaticFile(
|
||||||
try {
|
request: ExtendedRequest,
|
||||||
let filePath: string;
|
pathname: string,
|
||||||
|
ip: string,
|
||||||
|
): Promise<Response> {
|
||||||
|
let filePath: string;
|
||||||
|
let response: Response;
|
||||||
|
|
||||||
|
try {
|
||||||
if (pathname === "/favicon.ico") {
|
if (pathname === "/favicon.ico") {
|
||||||
filePath = resolve("public", "assets", "favicon.ico");
|
filePath = resolve("public", "assets", "favicon.ico");
|
||||||
} else {
|
} else {
|
||||||
|
@ -72,18 +75,49 @@ class ServerHandler {
|
||||||
|
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
const fileContent: ArrayBuffer = await file.arrayBuffer();
|
const fileContent: ArrayBuffer = await file.arrayBuffer();
|
||||||
const contentType: string = file.type || "application/octet-stream";
|
const contentType: string = file.type ?? "application/octet-stream";
|
||||||
|
|
||||||
return new Response(fileContent, {
|
response = new Response(fileContent, {
|
||||||
headers: { "Content-Type": contentType },
|
headers: { "Content-Type": contentType },
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
echo.warn(`File not found: ${filePath}`);
|
||||||
|
response = new Response("Not Found", { status: 404 });
|
||||||
}
|
}
|
||||||
logger.warn(`File not found: ${filePath}`);
|
|
||||||
return new Response("Not Found", { status: 404 });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error([`Error serving static file: ${pathname}`, error as Error]);
|
echo.error({
|
||||||
return new Response("Internal Server Error", { status: 500 });
|
message: `Error serving static file: ${pathname}`,
|
||||||
|
error: error as Error,
|
||||||
|
});
|
||||||
|
response = new Response("Internal Server Error", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logRequest(request, response, ip);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private logRequest(
|
||||||
|
request: ExtendedRequest,
|
||||||
|
response: Response,
|
||||||
|
ip: string | undefined,
|
||||||
|
): void {
|
||||||
|
const pathname = new URL(request.url).pathname;
|
||||||
|
|
||||||
|
const ignoredStartsWith: string[] = ["/public"];
|
||||||
|
const ignoredPaths: string[] = ["/favicon.ico"];
|
||||||
|
|
||||||
|
if (
|
||||||
|
ignoredStartsWith.some((prefix) => pathname.startsWith(prefix)) ||
|
||||||
|
ignoredPaths.includes(pathname)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo.custom(`${request.method}`, `${response.status}`, [
|
||||||
|
request.url,
|
||||||
|
`${(performance.now() - request.startPerf).toFixed(2)}ms`,
|
||||||
|
ip || "unknown",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRequest(
|
private async handleRequest(
|
||||||
|
@ -95,44 +129,44 @@ class ServerHandler {
|
||||||
|
|
||||||
const headers = request.headers;
|
const headers = request.headers;
|
||||||
let ip = server.requestIP(request)?.address;
|
let ip = server.requestIP(request)?.address;
|
||||||
|
let response: Response;
|
||||||
|
|
||||||
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
|
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
|
||||||
ip =
|
ip =
|
||||||
headers.get("CF-Connecting-IP")?.trim() ||
|
headers.get("CF-Connecting-IP")?.trim() ||
|
||||||
headers.get("X-Real-IP")?.trim() ||
|
headers.get("X-Real-IP")?.trim() ||
|
||||||
headers.get("X-Forwarded-For")?.split(",")[0].trim() ||
|
headers.get("X-Forwarded-For")?.split(",")[0]?.trim() ||
|
||||||
"unknown";
|
"unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathname: string = new URL(request.url).pathname;
|
const pathname: string = new URL(request.url).pathname;
|
||||||
if (pathname === "/robots.txt" && robotstxtPath) {
|
|
||||||
try {
|
const baseDir = resolve("public", "custom");
|
||||||
const file: BunFile = Bun.file(robotstxtPath);
|
const customPath = resolve(baseDir, pathname.slice(1));
|
||||||
if (await file.exists()) {
|
|
||||||
const fileContent: ArrayBuffer = await file.arrayBuffer();
|
if (!customPath.startsWith(baseDir)) {
|
||||||
const contentType: string = file.type || "text/plain";
|
response = new Response("Forbidden", { status: 403 });
|
||||||
return new Response(fileContent, {
|
this.logRequest(extendedRequest, response, ip);
|
||||||
headers: { "Content-Type": contentType },
|
return response;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
logger.warn(`File not found: ${robotstxtPath}`);
|
const customFile = Bun.file(customPath);
|
||||||
return new Response("Not Found", { status: 404 });
|
if (await customFile.exists()) {
|
||||||
} catch (error) {
|
const content = await customFile.arrayBuffer();
|
||||||
logger.error([
|
const type: string = customFile.type ?? "application/octet-stream";
|
||||||
`Error serving robots.txt: ${robotstxtPath}`,
|
response = new Response(content, {
|
||||||
error as Error,
|
headers: { "Content-Type": type },
|
||||||
]);
|
});
|
||||||
return new Response("Internal Server Error", { status: 500 });
|
this.logRequest(extendedRequest, response, ip);
|
||||||
}
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
|
if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
|
||||||
return await this.serveStaticFile(pathname);
|
return await this.serveStaticFile(extendedRequest, pathname, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
const match: MatchedRoute | null = this.router.match(request);
|
const match: MatchedRoute | null = this.router.match(request);
|
||||||
let requestBody: unknown = {};
|
let requestBody: unknown = {};
|
||||||
let response: Response;
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const { filePath, params, query } = match;
|
const { filePath, params, query } = match;
|
||||||
|
@ -141,7 +175,7 @@ class ServerHandler {
|
||||||
const routeModule: RouteModule = await import(filePath);
|
const routeModule: RouteModule = await import(filePath);
|
||||||
const contentType: string | null = request.headers.get("Content-Type");
|
const contentType: string | null = request.headers.get("Content-Type");
|
||||||
const actualContentType: string | null = contentType
|
const actualContentType: string | null = contentType
|
||||||
? contentType.split(";")[0].trim()
|
? (contentType.split(";")[0]?.trim() ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -230,7 +264,10 @@ class ServerHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error([`Error handling route ${request.url}:`, error as Error]);
|
echo.error({
|
||||||
|
message: `Error handling route ${request.url}`,
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
|
@ -252,20 +289,11 @@ class ServerHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.custom(
|
this.logRequest(extendedRequest, response, ip);
|
||||||
`[${request.method}]`,
|
|
||||||
`(${response.status})`,
|
|
||||||
[
|
|
||||||
request.url,
|
|
||||||
`${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`,
|
|
||||||
ip || "unknown",
|
|
||||||
],
|
|
||||||
"90",
|
|
||||||
);
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverHandler: ServerHandler = new ServerHandler(
|
const serverHandler: ServerHandler = new ServerHandler(
|
||||||
environment.port,
|
environment.port,
|
||||||
environment.host,
|
environment.host,
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
import { logger } from "@creations.works/logger";
|
import { echo } from "@atums/echo";
|
||||||
import type { ServerWebSocket } from "bun";
|
import type { ServerWebSocket } from "bun";
|
||||||
|
|
||||||
class WebSocketHandler {
|
class WebSocketHandler {
|
||||||
public handleMessage(ws: ServerWebSocket, message: string): void {
|
public handleMessage(ws: ServerWebSocket, message: string): void {
|
||||||
logger.info(`WebSocket received: ${message}`);
|
echo.info(`WebSocket received: ${message}`);
|
||||||
try {
|
try {
|
||||||
ws.send(`You said: ${message}`);
|
ws.send(`You said: ${message}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(["WebSocket send error", error as Error]);
|
echo.error({ message: "WebSocket send error", error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleOpen(ws: ServerWebSocket): void {
|
public handleOpen(ws: ServerWebSocket): void {
|
||||||
logger.info("WebSocket connection opened.");
|
echo.info("WebSocket connection opened.");
|
||||||
try {
|
try {
|
||||||
ws.send("Welcome to the WebSocket server!");
|
ws.send("Welcome to the WebSocket server!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(["WebSocket send error", error as Error]);
|
echo.error({ message: "WebSocket send error", error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleClose(ws: ServerWebSocket, code: number, reason: string): void {
|
public handleClose(_ws: ServerWebSocket, code: number, reason: string): void {
|
||||||
logger.warn(`WebSocket closed with code ${code}, reason: ${reason}`);
|
echo.warn(`WebSocket closed with code ${code}, reason: ${reason}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const webSocketHandler: WebSocketHandler = new WebSocketHandler();
|
const webSocketHandler: WebSocketHandler = new WebSocketHandler();
|
||||||
|
export { webSocketHandler, WebSocketHandler };
|
||||||
export { webSocketHandler };
|
|
||||||
|
|
|
@ -2,32 +2,38 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@*": ["src/*"],
|
||||||
"@config/*": ["config/*"],
|
"@config/*": ["config/*"],
|
||||||
"@types/*": ["types/*"],
|
"@types/*": ["types/*"],
|
||||||
"@helpers/*": ["src/helpers/*"]
|
"@helpers/*": ["src/helpers/*"]
|
||||||
},
|
},
|
||||||
"typeRoots": ["./src/types", "./node_modules/@types"],
|
"typeRoots": [
|
||||||
// Enable latest features
|
"./types",
|
||||||
"lib": ["ESNext", "DOM"],
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"allowJs": false,
|
||||||
"allowJs": true,
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": false,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
// Some stricter flags (disabled by default)
|
"noUnusedLocals": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedParameters": true,
|
||||||
"noUnusedParameters": false,
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
},
|
},
|
||||||
"include": ["src", "types", "config"]
|
"include": [
|
||||||
|
"src",
|
||||||
|
"types"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue