diff --git a/.forgejo/workflows/biomejs.yml b/.forgejo/workflows/biomejs.yml deleted file mode 100644 index 15c990c..0000000 --- a/.forgejo/workflows/biomejs.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Code quality checks - -on: - push: - pull_request: - -jobs: - biome: - runs-on: docker - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Bun - run: | - curl -fsSL https://bun.sh/install | bash - export BUN_INSTALL="$HOME/.bun" - echo "$BUN_INSTALL/bin" >> $GITHUB_PATH - - - name: Install Dependencies - run: bun install - - - name: Run Biome with verbose output - run: bunx biome ci . --verbose diff --git a/README.md b/README.md index fc41d6e..1b56686 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # bun frontend template -a simple bun frontend starting point i made and use +a simle bun frontend starting point i made and use diff --git a/biome.json b/biome.json deleted file mode 100644 index 921a7a5..0000000 --- a/biome.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$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" - } - } -} diff --git a/config/environment.ts b/config/environment.ts index cdd86e8..4b701df 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -1,6 +1,7 @@ export const environment: Environment = { - port: Number.parseInt(process.env.PORT || "8080", 10), + port: parseInt(process.env.PORT || "8080", 10), host: process.env.HOST || "0.0.0.0", development: - process.env.NODE_ENV === "development" || process.argv.includes("--dev"), + process.env.NODE_ENV === "development" || + process.argv.includes("--dev"), }; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d43df76 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,132 @@ +import pluginJs from "@eslint/js"; +import tseslintPlugin from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import promisePlugin from "eslint-plugin-promise"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import unicorn from "eslint-plugin-unicorn"; +import unusedImports from "eslint-plugin-unused-imports"; +import globals from "globals"; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + { + files: ["**/*.{js,mjs,cjs}"], + languageOptions: { + globals: globals.node, + }, + ...pluginJs.configs.recommended, + plugins: { + "simple-import-sort": simpleImportSort, + "unused-imports": unusedImports, + promise: promisePlugin, + prettier: prettier, + unicorn: unicorn, + }, + rules: { + "eol-last": ["error", "always"], + "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }], + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + "promise/always-return": "error", + "promise/no-return-wrap": "error", + "promise/param-names": "error", + "promise/catch-or-return": "error", + "promise/no-nesting": "warn", + "promise/no-promise-in-callback": "warn", + "promise/no-callback-in-promise": "warn", + "prettier/prettier": [ + "error", + { + useTabs: true, + tabWidth: 4, + }, + ], + indent: ["error", "tab", { SwitchCase: 1 }], + "unicorn/filename-case": [ + "error", + { + case: "camelCase", + }, + ], + }, + }, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser: tsParser, + globals: globals.node, + }, + plugins: { + "@typescript-eslint": tseslintPlugin, + "simple-import-sort": simpleImportSort, + "unused-imports": unusedImports, + promise: promisePlugin, + prettier: prettier, + unicorn: unicorn, + }, + rules: { + ...tseslintPlugin.configs.recommended.rules, + quotes: ["error", "double"], + "eol-last": ["error", "always"], + "no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }], + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + "promise/always-return": "error", + "promise/no-return-wrap": "error", + "promise/param-names": "error", + "promise/catch-or-return": "error", + "promise/no-nesting": "warn", + "promise/no-promise-in-callback": "warn", + "promise/no-callback-in-promise": "warn", + "prettier/prettier": [ + "error", + { + useTabs: true, + tabWidth: 4, + }, + ], + indent: ["error", "tab", { SwitchCase: 1 }], + "unicorn/filename-case": [ + "error", + { + case: "camelCase", + }, + ], + "@typescript-eslint/explicit-function-return-type": ["error"], + "@typescript-eslint/explicit-module-boundary-types": ["error"], + "@typescript-eslint/typedef": [ + "error", + { + arrowParameter: true, + variableDeclaration: true, + propertyDeclaration: true, + memberVariableDeclaration: true, + parameter: true, + }, + ], + }, + }, +]; diff --git a/package.json b/package.json index db5e8a9..a07273c 100644 --- a/package.json +++ b/package.json @@ -5,20 +5,25 @@ "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", + "lint": "eslint", + "lint:fix": "bun lint --fix", "cleanup": "rm -rf logs node_modules bun.lockdb" }, "devDependencies": { - "@types/bun": "^1.2.9", - "@types/ejs": "^3.1.5", - "globals": "^16.0.0", - "@biomejs/biome": "^1.9.4" + "@eslint/js": "^9.23.0", + "@types/bun": "^1.2.6", + "@typescript-eslint/eslint-plugin": "^8.28.0", + "@typescript-eslint/parser": "^8.28.0", + "eslint": "^9.23.0", + "eslint-plugin-prettier": "^5.2.5", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-unused-imports": "^4.1.4", + "globals": "^15.15.0", + "prettier": "^3.5.3" }, "peerDependencies": { - "typescript": "^5.8.3" - }, - "dependencies": { - "ejs": "^3.1.10" + "typescript": "^5.8.2" } } diff --git a/src/helpers/char.ts b/src/helpers/char.ts index 17657b3..6ecab40 100644 --- a/src/helpers/char.ts +++ b/src/helpers/char.ts @@ -1,6 +1,6 @@ export function timestampToReadable(timestamp?: number): string { const date: Date = - timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date(); - if (Number.isNaN(date.getTime())) return "Invalid Date"; + timestamp && !isNaN(timestamp) ? new Date(timestamp) : new Date(); + if (isNaN(date.getTime())) return "Invalid Date"; return date.toISOString().replace("T", " ").replace("Z", ""); } diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index 4cbb12b..331be1d 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -1,15 +1,15 @@ -import type { Stats } from "node:fs"; +import { environment } from "@config/environment"; +import { timestampToReadable } from "@helpers/char"; +import type { Stats } from "fs"; import { - type WriteStream, createWriteStream, existsSync, mkdirSync, statSync, -} from "node:fs"; -import { EOL } from "node:os"; -import { basename, join } from "node:path"; -import { environment } from "@config/environment"; -import { timestampToReadable } from "@helpers/char"; + WriteStream, +} from "fs"; +import { EOL } from "os"; +import { basename, join } from "path"; class Logger { private static instance: Logger; @@ -37,7 +37,7 @@ class Logger { mkdirSync(logDir, { recursive: true }); } - let addSeparator = false; + let addSeparator: boolean = false; if (existsSync(logFile)) { const fileStats: Stats = statSync(logFile); @@ -66,9 +66,9 @@ class Logger { private extractFileName(stack: string): string { const stackLines: string[] = stack.split("\n"); - let callerFile = ""; + let callerFile: string = ""; - for (let i = 2; i < stackLines.length; i++) { + for (let i: number = 2; i < stackLines.length; i++) { const line: string = stackLines[i].trim(); if (line && !line.includes("Logger.") && line.includes("(")) { callerFile = line.split("(")[1]?.split(")")[0] || ""; @@ -91,7 +91,7 @@ class Logger { return { filename, timestamp: readableTimestamp }; } - public info(message: string | string[], breakLine = false): void { + public info(message: string | string[], breakLine: boolean = false): void { const stack: string = new Error().stack || ""; const { filename, timestamp } = this.getCallerInfo(stack); @@ -110,7 +110,7 @@ class Logger { this.writeConsoleMessageColored(logMessageParts, breakLine); } - public warn(message: string | string[], breakLine = false): void { + public warn(message: string | string[], breakLine: boolean = false): void { const stack: string = new Error().stack || ""; const { filename, timestamp } = this.getCallerInfo(stack); @@ -131,7 +131,7 @@ class Logger { public error( message: string | Error | (string | Error)[], - breakLine = false, + breakLine: boolean = false, ): void { const stack: string = new Error().stack || ""; const { filename, timestamp } = this.getCallerInfo(stack); @@ -161,7 +161,7 @@ class Logger { bracketMessage2: string, message: string | string[], color: string, - breakLine = false, + breakLine: boolean = false, ): void { const stack: string = new Error().stack || ""; const { timestamp } = this.getCallerInfo(stack); @@ -189,7 +189,7 @@ class Logger { private writeConsoleMessageColored( logMessageParts: ILogMessageParts, - breakLine = false, + breakLine: boolean = false, ): void { const logMessage: string = Object.keys(logMessageParts) .map((key: string) => { diff --git a/src/index.ts b/src/index.ts index 60606d4..6d2801d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,11 @@ import { logger } from "@helpers/logger"; import { serverHandler } from "@/server"; async function main(): Promise { - serverHandler.initialize(); + try { + serverHandler.initialize(); + } catch (error) { + throw error; + } } main().catch((error: Error) => { diff --git a/src/server.ts b/src/server.ts index 909813c..3f78cb5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,3 @@ -import { resolve } from "node:path"; import { environment } from "@config/environment"; import { logger } from "@helpers/logger"; import { @@ -7,6 +6,7 @@ import { type MatchedRoute, type Serve, } from "bun"; +import { resolve } from "path"; import { webSocketHandler } from "@/websocket"; @@ -73,16 +73,21 @@ class ServerHandler { if (await file.exists()) { 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, { headers: { "Content-Type": contentType }, }); + } else { + logger.warn(`File not found: ${filePath}`); + return new Response("Not Found", { status: 404 }); } - 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]); + logger.error([ + `Error serving static file: ${pathname}`, + error as Error, + ]); return new Response("Internal Server Error", { status: 500 }); } } @@ -108,7 +113,8 @@ class ServerHandler { try { 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 ? contentType.split(";")[0].trim() : null; @@ -135,7 +141,9 @@ class ServerHandler { if ( (Array.isArray(routeModule.routeDef.method) && - !routeModule.routeDef.method.includes(request.method)) || + !routeModule.routeDef.method.includes( + request.method, + )) || (!Array.isArray(routeModule.routeDef.method) && routeModule.routeDef.method !== request.method) ) { @@ -160,7 +168,9 @@ class ServerHandler { if (Array.isArray(expectedContentType)) { matchesAccepts = expectedContentType.includes("*/*") || - expectedContentType.includes(actualContentType || ""); + expectedContentType.includes( + actualContentType || "", + ); } else { matchesAccepts = expectedContentType === "*/*" || @@ -199,7 +209,10 @@ class ServerHandler { } } } catch (error: unknown) { - logger.error([`Error handling route ${request.url}:`, error as Error]); + logger.error([ + `Error handling route ${request.url}:`, + error as Error, + ]); response = Response.json( { @@ -221,15 +234,15 @@ class ServerHandler { ); } - const headers = request.headers; - let ip = server.requestIP(request)?.address; + const headers: Headers = response.headers; + let ip: string | null = server.requestIP(request)?.address || null; - if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") { + if (!ip) { ip = - headers.get("CF-Connecting-IP")?.trim() || - headers.get("X-Real-IP")?.trim() || - headers.get("X-Forwarded-For")?.split(",")[0].trim() || - "unknown"; + headers.get("CF-Connecting-IP") || + headers.get("X-Real-IP") || + headers.get("X-Forwarded-For") || + null; } logger.custom( diff --git a/src/websocket.ts b/src/websocket.ts index 99686e8..ce87fe8 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,5 +1,5 @@ import { logger } from "@helpers/logger"; -import type { ServerWebSocket } from "bun"; +import { type ServerWebSocket } from "bun"; class WebSocketHandler { public handleMessage(ws: ServerWebSocket, message: string): void { @@ -20,7 +20,11 @@ class WebSocketHandler { } } - 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}`); } } diff --git a/tsconfig.json b/tsconfig.json index 68a5a97..ac5f2c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,28 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "@/*": ["src/*"], - "@config/*": ["config/*"], - "@types/*": ["types/*"], - "@helpers/*": ["src/helpers/*"] + "@/*": [ + "src/*" + ], + "@config/*": [ + "config/*" + ], + "@types/*": [ + "types/*" + ], + "@helpers/*": [ + "src/helpers/*" + ] }, - "typeRoots": ["./src/types", "./node_modules/@types"], + "typeRoots": [ + "./src/types", + "./node_modules/@types" + ], // Enable latest features - "lib": ["ESNext", "DOM"], + "lib": [ + "ESNext", + "DOM" + ], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", @@ -27,7 +41,11 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, }, - "include": ["src", "types", "config"] + "include": [ + "src", + "types", + "config" + ], }