@kichu12348/bunify 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -0
- package/benchmarks/bun-raw.ts +54 -0
- package/benchmarks/bunify.ts +43 -0
- package/bun.lock +26 -0
- package/example.ts +11 -0
- package/index.ts +4 -0
- package/package.json +23 -0
- package/src/core.ts +377 -0
- package/tsconfig.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Bunify
|
|
2
|
+
|
|
3
|
+
A fast, lightweight, and strictly-typed web framework built specifically for [Bun](https://bun.sh/).
|
|
4
|
+
|
|
5
|
+
**Bunify** offers a developer-friendly API heavily inspired by proven web frameworks like Fastify, but optimized for the modern Bun ecosystem. It features robust typing, lifecycle hooks, powerful routing, and middleware support right out of the box.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Since Bunify relies on native Bun features, ensure you have [Bun installed](https://bun.sh/).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add bunify
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Bunify } from "bunify";
|
|
19
|
+
|
|
20
|
+
const app = new Bunify({ logger: true });
|
|
21
|
+
|
|
22
|
+
app.get("/hello", (request, reply) => {
|
|
23
|
+
return { message: "Hello, World!" };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.listen(3000, (address) => {
|
|
27
|
+
console.log(`Server is running at http://${address}`);
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
### Type-Safe Routing
|
|
34
|
+
Bunify enforces strict typing on your `return` statements and incoming requests using `RouteSchema` definition.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
type GreetRoute = {
|
|
38
|
+
Body: { name: string };
|
|
39
|
+
Query: { formal?: string };
|
|
40
|
+
Params: { id: string };
|
|
41
|
+
Reply: { greeting: string };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
app.post<GreetRoute>("/greet/:id", async (request, reply) => {
|
|
45
|
+
const body = await request.json(); // Strongly typed to { name: string }
|
|
46
|
+
const formal = request.query.formal;
|
|
47
|
+
const id = request.params.id; // Strongly typed to string
|
|
48
|
+
|
|
49
|
+
return { greeting: `Hello ${body.name}, your id is ${id}` };
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Middleware (`use`)
|
|
54
|
+
You can use middleware to intercept incoming requests and process them before they hit your routes. Note that you MUST call `next()` to proceed.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
app.use(async (request, reply, next) => {
|
|
58
|
+
console.log(`Incoming request: ${request.method} ${request.url}`);
|
|
59
|
+
await next();
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Lifecycle Hooks
|
|
64
|
+
Tap into requests at specific lifecycles. Available hooks are `onRequest`, `preHandler`, and `onResponse`.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
app.addHook("onRequest", async (request, reply, next) => {
|
|
68
|
+
// Logic here
|
|
69
|
+
await next();
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Routing & Chaining Middlewares
|
|
74
|
+
You can apply middleware locally per-route by chaining them before the final handler.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
app.get(
|
|
78
|
+
"/protected",
|
|
79
|
+
async (request, reply, next) => {
|
|
80
|
+
if (!request.headers.get("Authorization")) {
|
|
81
|
+
return reply.status(401).json({ error: "Unauthorized" });
|
|
82
|
+
}
|
|
83
|
+
await next();
|
|
84
|
+
},
|
|
85
|
+
(request, reply) => {
|
|
86
|
+
return { data: "Top Secret" };
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Decorators
|
|
92
|
+
Easily attach custom utilities, instances, or metadata to your `Bunify` application context and requests.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// Define expected decorators
|
|
96
|
+
type AppDecorators = { db: any };
|
|
97
|
+
|
|
98
|
+
const app = new Bunify<AppDecorators>();
|
|
99
|
+
|
|
100
|
+
app.decorate("db", { getDocs: () => [{ id: 1 }] });
|
|
101
|
+
|
|
102
|
+
app.get("/docs", (request, reply) => {
|
|
103
|
+
// request.db is available and correctly typed
|
|
104
|
+
return request.db.getDocs();
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Plugins / Sub-Routers
|
|
109
|
+
Divide your application into logical modules combining prefixes, scoped dependencies, and decorators securely using `.register()`.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
app.register(
|
|
113
|
+
(adminApp) => {
|
|
114
|
+
adminApp.get("/dashboard", (request, reply) => {
|
|
115
|
+
reply.html("<h1>Admin Dashboard</h1>");
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
{ prefix: "/admin" } // accessible at /admin/dashboard
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Reply Methods
|
|
123
|
+
|
|
124
|
+
The `reply` object gives you fine-grained control to shape responses gracefully.
|
|
125
|
+
|
|
126
|
+
- `reply.status(code)` / `reply.code(code)`: Set the HTTP status code
|
|
127
|
+
- `reply.headers(key, value)`: Insert a new Header
|
|
128
|
+
- `reply.json(data)`: Automatically sends a JSON response
|
|
129
|
+
- `reply.html(content)`: Easily serve HTML tags
|
|
130
|
+
- `reply.redirect(url)`: Responds with a 302 redirect
|
|
131
|
+
- `reply.send(content)`: Generic send handler
|
|
132
|
+
- Return directly from `handler` instead of returning `reply.send()` (See Quick Start).
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const server = Bun.serve({
|
|
2
|
+
port: 3001,
|
|
3
|
+
async fetch(req) {
|
|
4
|
+
const url = new URL(req.url);
|
|
5
|
+
|
|
6
|
+
// 1. Simulate Middleware (headers added to all routes)
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
headers.set("X-Benchmark-Time", Date.now().toString());
|
|
9
|
+
|
|
10
|
+
if (req.method === "GET") {
|
|
11
|
+
// 2. Simple GET
|
|
12
|
+
if (url.pathname === "/") {
|
|
13
|
+
headers.set("Content-Type", "application/json");
|
|
14
|
+
return new Response(JSON.stringify({ message: "Hello, World!" }), {
|
|
15
|
+
headers,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 3. Query params map handler
|
|
20
|
+
if (url.pathname === "/search") {
|
|
21
|
+
headers.set("Content-Type", "application/json");
|
|
22
|
+
const q = url.searchParams.get("q") || "none";
|
|
23
|
+
return new Response(JSON.stringify({ results: q }), { headers });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 4. Larger JSON Payload
|
|
27
|
+
if (url.pathname === "/data") {
|
|
28
|
+
headers.set("Content-Type", "application/json");
|
|
29
|
+
return new Response(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
items: [
|
|
32
|
+
{ id: 1, name: "Item A" },
|
|
33
|
+
{ id: 2, name: "Item B" },
|
|
34
|
+
{ id: 3, name: "Item C" },
|
|
35
|
+
],
|
|
36
|
+
status: "ok",
|
|
37
|
+
}),
|
|
38
|
+
{ headers },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 5. POST body JSON
|
|
44
|
+
if (req.method === "POST" && url.pathname === "/echo") {
|
|
45
|
+
const body = await req.json();
|
|
46
|
+
headers.set("Content-Type", "application/json");
|
|
47
|
+
return new Response(JSON.stringify(body), { status: 201, headers });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new Response("Not Found", { status: 404 });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log(`Raw Bun server listening on ${server.hostname}:${server.port}`);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Bunify } from "../src/core";
|
|
2
|
+
|
|
3
|
+
const app = new Bunify();
|
|
4
|
+
|
|
5
|
+
// 1. App-level Middleware
|
|
6
|
+
app.use(async (req, reply, next) => {
|
|
7
|
+
reply.headers("X-Benchmark-Time", Date.now().toString());
|
|
8
|
+
return next();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// 2. Simple GET
|
|
12
|
+
app.get("/", () => {
|
|
13
|
+
return { message: "Hello, World!" };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// 3. Query params map handler
|
|
17
|
+
app.get("/search", (req) => {
|
|
18
|
+
return {
|
|
19
|
+
results: req.query.q || "none",
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 4. Larger JSON Payload
|
|
24
|
+
app.get("/data", () => {
|
|
25
|
+
return {
|
|
26
|
+
items: [
|
|
27
|
+
{ id: 1, name: "Item A" },
|
|
28
|
+
{ id: 2, name: "Item B" },
|
|
29
|
+
{ id: 3, name: "Item C" },
|
|
30
|
+
],
|
|
31
|
+
status: "ok",
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 5. POST body JSON
|
|
36
|
+
app.post("/echo", async (req, reply) => {
|
|
37
|
+
const body = await req.json();
|
|
38
|
+
return reply.status(201).json(body);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
app.listen(3000, (url) => {
|
|
42
|
+
console.log(`Bunify listening on ${url}`);
|
|
43
|
+
});
|
package/bun.lock
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "bunify",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"typescript": "^5.9.3",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
"packages": {
|
|
16
|
+
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
|
17
|
+
|
|
18
|
+
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
|
19
|
+
|
|
20
|
+
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
|
21
|
+
|
|
22
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
23
|
+
|
|
24
|
+
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
25
|
+
}
|
|
26
|
+
}
|
package/example.ts
ADDED
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kichu12348/bunify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": "Kichu",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"module": "index.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./index.ts",
|
|
13
|
+
"types": "./index.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.0.0",
|
|
18
|
+
"@types/bun": "latest"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"typescript": "^5.9.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
2
|
+
|
|
3
|
+
type RouteSchema = {
|
|
4
|
+
Query?: Record<string, any>;
|
|
5
|
+
Params?: Record<string, any>;
|
|
6
|
+
Body?: any;
|
|
7
|
+
Reply?: any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BunifyRequest<T extends RouteSchema = RouteSchema, D = {}> = {
|
|
11
|
+
raw: Bun.BunRequest;
|
|
12
|
+
query: Record<string, string> & (T["Query"] extends object ? T["Query"] : {});
|
|
13
|
+
params: T["Params"] extends infer U
|
|
14
|
+
? Record<string, string> & U
|
|
15
|
+
: Record<string, string>;
|
|
16
|
+
json: () => Promise<T["Body"]>;
|
|
17
|
+
text: () => Promise<string>;
|
|
18
|
+
headers: Headers;
|
|
19
|
+
method: string;
|
|
20
|
+
url: string;
|
|
21
|
+
} & D;
|
|
22
|
+
|
|
23
|
+
type Reply = ReturnType<typeof createReply>;
|
|
24
|
+
|
|
25
|
+
export type BunifyReply<T extends RouteSchema = RouteSchema> = Reply & {
|
|
26
|
+
send: (content: T["Reply"] | Response) => Response;
|
|
27
|
+
json: (data: T["Reply"]) => Response;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type Handler<T extends RouteSchema = RouteSchema, D = {}> = (
|
|
31
|
+
request: BunifyRequest<T, D>,
|
|
32
|
+
reply: BunifyReply<T>,
|
|
33
|
+
) => T["Reply"] | Response | Promise<T["Reply"] | Response>;
|
|
34
|
+
|
|
35
|
+
export type Middleware<T extends RouteSchema = RouteSchema, D = {}> = (
|
|
36
|
+
request: BunifyRequest<T, D>,
|
|
37
|
+
reply: BunifyReply<T>,
|
|
38
|
+
next: () => Promise<any>,
|
|
39
|
+
) => any;
|
|
40
|
+
|
|
41
|
+
type HookType = "onRequest" | "preHandler" | "onResponse";
|
|
42
|
+
|
|
43
|
+
export interface BunifyInstance {
|
|
44
|
+
get<T extends RouteSchema = RouteSchema>(
|
|
45
|
+
path: string,
|
|
46
|
+
handler: Handler<T>,
|
|
47
|
+
): void;
|
|
48
|
+
post<T extends RouteSchema = RouteSchema>(
|
|
49
|
+
path: string,
|
|
50
|
+
handler: Handler<T>,
|
|
51
|
+
): void;
|
|
52
|
+
put<T extends RouteSchema = RouteSchema>(
|
|
53
|
+
path: string,
|
|
54
|
+
handler: Handler<T>,
|
|
55
|
+
): void;
|
|
56
|
+
delete<T extends RouteSchema = RouteSchema>(
|
|
57
|
+
path: string,
|
|
58
|
+
handler: Handler<T>,
|
|
59
|
+
): void;
|
|
60
|
+
patch<T extends RouteSchema = RouteSchema>(
|
|
61
|
+
path: string,
|
|
62
|
+
handler: Handler<T>,
|
|
63
|
+
): void;
|
|
64
|
+
listen(port: number, callback?: (address: string) => void): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface RegisterOptions {
|
|
68
|
+
prefix?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createReply() {
|
|
72
|
+
let status = 200;
|
|
73
|
+
const headers = new Headers();
|
|
74
|
+
let body: any = null;
|
|
75
|
+
let sent = false;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
get sent() {
|
|
79
|
+
return sent;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
status(code: number) {
|
|
83
|
+
status = code;
|
|
84
|
+
return this;
|
|
85
|
+
},
|
|
86
|
+
code(code: number) {
|
|
87
|
+
return this.status(code);
|
|
88
|
+
},
|
|
89
|
+
headers(key: string, value: string) {
|
|
90
|
+
headers.set(key, value);
|
|
91
|
+
return this;
|
|
92
|
+
},
|
|
93
|
+
json(data: any) {
|
|
94
|
+
return this.headers("Content-Type", "application/json").send(data);
|
|
95
|
+
},
|
|
96
|
+
redirect(url: string) {
|
|
97
|
+
return this.status(302).headers("Location", url).send(null);
|
|
98
|
+
},
|
|
99
|
+
html(content: string) {
|
|
100
|
+
return this.headers("Content-Type", "text/html").send(content);
|
|
101
|
+
},
|
|
102
|
+
send(content: any) {
|
|
103
|
+
if (sent) {
|
|
104
|
+
throw new Error("Reply has already been sent.");
|
|
105
|
+
}
|
|
106
|
+
sent = true;
|
|
107
|
+
if (content instanceof Response) return content;
|
|
108
|
+
|
|
109
|
+
if (typeof content === "object") {
|
|
110
|
+
headers.set("Content-Type", "application/json");
|
|
111
|
+
content = JSON.stringify(content);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
body = content;
|
|
115
|
+
return this.build();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
build() {
|
|
119
|
+
if (body instanceof Response) return body;
|
|
120
|
+
|
|
121
|
+
return new Response(body, { status, headers });
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class Bunify<Decorators extends Record<string, any> = {}> {
|
|
127
|
+
private logger: boolean;
|
|
128
|
+
private prefix: string = "";
|
|
129
|
+
private routes: { [key: string]: any };
|
|
130
|
+
private middlewares: Middleware<any, Decorators>[] = [];
|
|
131
|
+
private hooks: Record<HookType, Middleware<any, Decorators>[]> = {
|
|
132
|
+
onRequest: [],
|
|
133
|
+
preHandler: [],
|
|
134
|
+
onResponse: [],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
private decorators: Decorators = {} as Decorators;
|
|
138
|
+
|
|
139
|
+
constructor(options?: { logger?: boolean }) {
|
|
140
|
+
this.logger = options?.logger ?? false;
|
|
141
|
+
this.routes = {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private joinPaths(base: string, path: string) {
|
|
145
|
+
return (base + path).replace(/\/+/g, "/");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private addRoute<T extends RouteSchema = RouteSchema>(
|
|
149
|
+
method: Method,
|
|
150
|
+
path: string,
|
|
151
|
+
handlers: (Handler<T, Decorators> | Middleware<T, Decorators>)[],
|
|
152
|
+
) {
|
|
153
|
+
const { handler, middlewares } = this.parseHandlers<T>(handlers);
|
|
154
|
+
const composedHandler = this.compose<T>(
|
|
155
|
+
[...this.middlewares, ...middlewares] as Middleware<T, Decorators>[],
|
|
156
|
+
handler,
|
|
157
|
+
);
|
|
158
|
+
const wrappedHandler = async (req: Bun.BunRequest) => {
|
|
159
|
+
const url = new URL(req.url);
|
|
160
|
+
|
|
161
|
+
const request: BunifyRequest<T, Decorators> = {
|
|
162
|
+
raw: req,
|
|
163
|
+
query: Object.fromEntries(url.searchParams.entries()) as BunifyRequest<
|
|
164
|
+
T,
|
|
165
|
+
Decorators
|
|
166
|
+
>["query"],
|
|
167
|
+
params: (req.params || {}) as BunifyRequest<T, Decorators>["params"],
|
|
168
|
+
json: () => req.json(),
|
|
169
|
+
text: () => req.text(),
|
|
170
|
+
headers: req.headers,
|
|
171
|
+
method: req.method,
|
|
172
|
+
url: req.url,
|
|
173
|
+
...(this.decorators as Decorators),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const reply = createReply() as BunifyReply<T>;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const res = await this.runPipeline(request, reply, composedHandler);
|
|
180
|
+
if (!reply.sent && res !== undefined) {
|
|
181
|
+
return reply.send(res);
|
|
182
|
+
}
|
|
183
|
+
return reply.build();
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (this.logger) {
|
|
186
|
+
console.error(`Error in [${method} ${path}]:`, err);
|
|
187
|
+
}
|
|
188
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const fullPath = this.joinPaths(this.prefix, path);
|
|
193
|
+
|
|
194
|
+
const route = this.routes[fullPath];
|
|
195
|
+
if (!route) {
|
|
196
|
+
this.routes[fullPath] = { [method]: wrappedHandler };
|
|
197
|
+
} else {
|
|
198
|
+
this.routes[fullPath][method] = wrappedHandler;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private compose<T extends RouteSchema>(
|
|
203
|
+
middlewares: Middleware<T, Decorators>[],
|
|
204
|
+
handler: Handler<T, Decorators>,
|
|
205
|
+
) {
|
|
206
|
+
return async (
|
|
207
|
+
request: BunifyRequest<T, Decorators>,
|
|
208
|
+
reply: BunifyReply<T>,
|
|
209
|
+
) => {
|
|
210
|
+
let index = -1;
|
|
211
|
+
const next = async (i: number): Promise<any> => {
|
|
212
|
+
if (i <= index) {
|
|
213
|
+
throw new Error("next() called multiple times");
|
|
214
|
+
}
|
|
215
|
+
index = i;
|
|
216
|
+
|
|
217
|
+
if (i === middlewares.length) {
|
|
218
|
+
return handler(request, reply);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const mw = middlewares[i];
|
|
222
|
+
if (mw) {
|
|
223
|
+
return mw(request, reply, () => next(i + 1));
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
return next(0);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private parseHandlers<T extends RouteSchema = RouteSchema>(
|
|
231
|
+
handlers: (Handler<T, Decorators> | Middleware<T, Decorators>)[],
|
|
232
|
+
) {
|
|
233
|
+
const flat = handlers.flat();
|
|
234
|
+
const handler = flat[flat.length - 1] as Handler<T, Decorators>;
|
|
235
|
+
const middlewares = flat.slice(0, -1) as Middleware<T, Decorators>[];
|
|
236
|
+
return { handler, middlewares };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private runHooks<T extends RouteSchema>(
|
|
240
|
+
hooks: Middleware<T, Decorators>[],
|
|
241
|
+
request: BunifyRequest<T, Decorators>,
|
|
242
|
+
reply: BunifyReply<T>,
|
|
243
|
+
) {
|
|
244
|
+
let index = -1;
|
|
245
|
+
|
|
246
|
+
const next = async (i: number): Promise<any> => {
|
|
247
|
+
if (i <= index) {
|
|
248
|
+
throw new Error("next() called multiple times in hooks");
|
|
249
|
+
}
|
|
250
|
+
index = i;
|
|
251
|
+
|
|
252
|
+
const fn = hooks[i];
|
|
253
|
+
if (!fn) return;
|
|
254
|
+
|
|
255
|
+
return fn(request, reply, () => next(i + 1));
|
|
256
|
+
};
|
|
257
|
+
return next(0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async runPipeline<T extends RouteSchema>(
|
|
261
|
+
request: BunifyRequest<T, Decorators>,
|
|
262
|
+
reply: BunifyReply<T>,
|
|
263
|
+
handler: Handler<T, Decorators>,
|
|
264
|
+
) {
|
|
265
|
+
let res = await this.runHooks(
|
|
266
|
+
this.hooks.onRequest as Middleware<T, Decorators>[],
|
|
267
|
+
request,
|
|
268
|
+
reply,
|
|
269
|
+
);
|
|
270
|
+
if (reply.sent) return;
|
|
271
|
+
|
|
272
|
+
res = await this.runHooks(
|
|
273
|
+
this.hooks.preHandler as Middleware<T, Decorators>[],
|
|
274
|
+
request,
|
|
275
|
+
reply,
|
|
276
|
+
);
|
|
277
|
+
if (reply.sent) return;
|
|
278
|
+
|
|
279
|
+
res = await handler(request, reply);
|
|
280
|
+
if (reply.sent) return res;
|
|
281
|
+
|
|
282
|
+
await this.runHooks(
|
|
283
|
+
this.hooks.onResponse as Middleware<T, Decorators>[],
|
|
284
|
+
request,
|
|
285
|
+
reply,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return res;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
decorate<K extends string, V>(name: K, value: V) {
|
|
292
|
+
if ((this.decorators as any)[name]) {
|
|
293
|
+
throw new Error(`Decorator '${name}' already exists.`);
|
|
294
|
+
}
|
|
295
|
+
(this.decorators as any)[name] = value;
|
|
296
|
+
|
|
297
|
+
return this as any;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
register(
|
|
301
|
+
fn: (app: Bunify<Decorators>) => void,
|
|
302
|
+
{ prefix = "" }: RegisterOptions,
|
|
303
|
+
) {
|
|
304
|
+
const child = new Bunify<Decorators>({ logger: this.logger });
|
|
305
|
+
child.routes = this.routes; // Share routes
|
|
306
|
+
child.prefix = this.joinPaths(this.prefix, prefix);
|
|
307
|
+
|
|
308
|
+
child.hooks = {
|
|
309
|
+
onRequest: [...this.hooks.onRequest],
|
|
310
|
+
preHandler: [...this.hooks.preHandler],
|
|
311
|
+
onResponse: [...this.hooks.onResponse],
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
child.decorators = { ...this.decorators };
|
|
315
|
+
|
|
316
|
+
child.middlewares = [...(this.middlewares || [])];
|
|
317
|
+
fn(child);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
use(fn: Middleware<any, Decorators>) {
|
|
321
|
+
this.middlewares.push(fn);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
addHook(type: HookType, fn: Middleware<any, Decorators>) {
|
|
325
|
+
this.hooks[type].push(fn);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get<T extends RouteSchema = RouteSchema>(
|
|
329
|
+
path: string,
|
|
330
|
+
...handlers: [...Middleware<T, Decorators>[], Handler<T, Decorators>]
|
|
331
|
+
) {
|
|
332
|
+
this.addRoute("GET", path, handlers);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
post<T extends RouteSchema = RouteSchema>(
|
|
336
|
+
path: string,
|
|
337
|
+
...handlers: [...Middleware<T, Decorators>[], Handler<T, Decorators>]
|
|
338
|
+
) {
|
|
339
|
+
this.addRoute("POST", path, handlers);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
put<T extends RouteSchema = RouteSchema>(
|
|
343
|
+
path: string,
|
|
344
|
+
...handlers: [...Middleware<T, Decorators>[], Handler<T, Decorators>]
|
|
345
|
+
) {
|
|
346
|
+
this.addRoute("PUT", path, handlers);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
delete<T extends RouteSchema = RouteSchema>(
|
|
350
|
+
path: string,
|
|
351
|
+
...handlers: [...Middleware<T, Decorators>[], Handler<T, Decorators>]
|
|
352
|
+
) {
|
|
353
|
+
this.addRoute("DELETE", path, handlers);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
patch<T extends RouteSchema = RouteSchema>(
|
|
357
|
+
path: string,
|
|
358
|
+
...handlers: [...Middleware<T, Decorators>[], Handler<T, Decorators>]
|
|
359
|
+
) {
|
|
360
|
+
this.addRoute("PATCH", path, handlers);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
listen(port: number, callback?: (address: string) => void) {
|
|
364
|
+
const server = Bun.serve({
|
|
365
|
+
port: port,
|
|
366
|
+
routes: this.routes,
|
|
367
|
+
fetch: async () => {
|
|
368
|
+
return new Response("Not Found", { status: 404 });
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
if (callback) {
|
|
372
|
+
callback(server.hostname + ":" + server.port);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return server;
|
|
376
|
+
}
|
|
377
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"types": ["bun"],
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
// Best practices
|
|
19
|
+
"strict": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
|
|
25
|
+
// Some stricter flags (disabled by default)
|
|
26
|
+
"noUnusedLocals": false,
|
|
27
|
+
"noUnusedParameters": false,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false
|
|
29
|
+
}
|
|
30
|
+
}
|