diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..b9762ca Binary files /dev/null and b/bun.lockb differ diff --git a/lib/core/helpers.ts b/lib/core/helpers.ts new file mode 100644 index 0000000..8452ed2 --- /dev/null +++ b/lib/core/helpers.ts @@ -0,0 +1,37 @@ +export function parseURI(uri: string): { + query: string; + filters: { [key: string]: string }; +} { + const { pathname: query, searchParams } = new URL(uri); + const filters = Object.fromEntries(searchParams); + return { query, filters }; +} + +export function match(query: string, template: string): boolean { + const queryEntries = query.split("/").filter(Boolean); + const templateEntries = template.split("/").filter(Boolean); + if (queryEntries.length !== templateEntries.length) return false; + const filteredQueryEntries = queryEntries.filter((entry) => + isNaN(Number(entry)) + ); + const filteredTemplateEntries = templateEntries.filter( + (entry) => !entry.startsWith(":") + ); + return filteredQueryEntries.join("/") === filteredTemplateEntries.join("/"); +} + +export function parseParams( + query: string, + template: string +): { [key: string]: string } { + const result: { [key: string]: string } = {}; + const queryEntries = query.split("/"); + const templateEntries = template.split("/"); + for (const [index, entry] of templateEntries.entries()) { + if (!entry.startsWith(":")) continue; + const key = entry.substring(1); + const value = queryEntries[index]; + result[key] = value; + } + return result; +} diff --git a/lib/core/http.ts b/lib/core/http.ts new file mode 100644 index 0000000..122f5b4 --- /dev/null +++ b/lib/core/http.ts @@ -0,0 +1,10 @@ +export type HTTPMethod = + | "GET" + | "POST" + | "PUT" + | "PATCH" + | "DELETE" + | "HEAD" + | "OPTIONS" + | "CONNECT" + | "TRACE"; diff --git a/lib/router.ts b/lib/router.ts new file mode 100644 index 0000000..bb7e1f4 --- /dev/null +++ b/lib/router.ts @@ -0,0 +1,42 @@ +import { match, parseParams, parseURI } from "./core/helpers"; +import { HTTPMethod } from "./core/http"; +import { Handler } from "./types"; + +interface Endpoint { + method: HTTPMethod; + template: string; + handler: Handler; +} + +export default class Router { + endpoints: Endpoint[] = []; + + register(method: HTTPMethod, template: string, handler: Handler) { + this.endpoints.push({ method, template, handler }); + } + + async handle(request: Request): Promise { + const { method, url } = request; + const { query, filters } = parseURI(url); + const endpoint = this.endpoints.find( + ({ method: endpointMethod, template }) => + endpointMethod === method && match(query, template) + ); + if (!endpoint) return new Response(null, { status: 404 }); + const body = await request.json(); + const { handler, template } = endpoint; + const params = parseParams(query, template); + let result; + try { + result = await handler({ body, filters, params, request }); + } catch (error: unknown) { + const message = (error as Error).message; + return new Response(message, { status: 500 }); + } + if (result instanceof Response) return result; + else if (typeof result === "string") return new Response(result); + else if (typeof result === "object") + return new Response(JSON.stringify(result)); + else return new Response(); + } +} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..71a8a60 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,6 @@ +export type Handler = (props: { + params: { [key: string]: string }; + filters: { [key: string]: string }; + body: object | undefined; + request: Request; +}) => Response | object | string | void; diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..33ed013 --- /dev/null +++ b/main.ts @@ -0,0 +1,3 @@ +import Router from "./lib/router"; + +export const router = new Router(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..ebe76f7 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "@genericst/bun-router", + "module": "main.ts", + "type": "module", + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c937271 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "preserve", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" + ] + } +}