@typokit/platform-bun 0.1.4

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.
@@ -0,0 +1,53 @@
1
+ import type { ServerHandle, TypoKitRequest, TypoKitResponse } from "@typokit/types";
2
+ /** Subset of Bun's Server type we rely on */
3
+ interface BunServer {
4
+ port: number;
5
+ hostname: string;
6
+ stop(closeActiveConnections?: boolean): void;
7
+ }
8
+ /** Runtime platform metadata for diagnostics and inspect commands */
9
+ export interface PlatformInfo {
10
+ runtime: string;
11
+ version: string;
12
+ }
13
+ /** Returns Bun platform info */
14
+ export declare function getPlatformInfo(): PlatformInfo;
15
+ /**
16
+ * Normalize a Web API Request (used by Bun.serve) into a TypoKitRequest.
17
+ */
18
+ export declare function normalizeRequest(req: Request): Promise<TypoKitRequest>;
19
+ /**
20
+ * Convert a TypoKitResponse into a Web API Response for Bun.serve().
21
+ */
22
+ export declare function buildResponse(response: TypoKitResponse): Response;
23
+ /** Handler function that receives a normalized request and returns a response */
24
+ export type BunRequestHandler = (req: TypoKitRequest) => Promise<TypoKitResponse>;
25
+ export interface BunServerOptions {
26
+ /** Optional hostname to bind to (default: "0.0.0.0") */
27
+ hostname?: string;
28
+ }
29
+ /** Result of createServer — provides listen/close and access to the underlying Bun server */
30
+ export interface BunServerInstance {
31
+ /** Start listening on the given port. Returns a handle for graceful shutdown. */
32
+ listen(port: number): Promise<ServerHandle>;
33
+ /** The underlying Bun server instance (available after listen()) */
34
+ server: BunServer | null;
35
+ }
36
+ /**
37
+ * Create a Bun HTTP server that dispatches to a TypoKit request handler.
38
+ *
39
+ * Usage:
40
+ * ```ts
41
+ * const srv = createServer(async (req) => ({
42
+ * status: 200,
43
+ * headers: {},
44
+ * body: { ok: true },
45
+ * }));
46
+ * const handle = await srv.listen(3000);
47
+ * // ... later
48
+ * await handle.close();
49
+ * ```
50
+ */
51
+ export declare function createServer(handler: BunRequestHandler, options?: BunServerOptions): BunServerInstance;
52
+ export {};
53
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,YAAY,EACZ,cAAc,EACd,eAAe,EAChB,MAAM,gBAAgB,CAAC;AAMxB,6CAA6C;AAC7C,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,sBAAsB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9C;AAiBD,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gCAAgC;AAChC,wBAAgB,eAAe,IAAI,YAAY,CAM9C;AAiCD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CA4B5E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,eAAe,GAAG,QAAQ,CA8BjE;AAID,iFAAiF;AACjF,MAAM,MAAM,iBAAiB,GAAG,CAC9B,GAAG,EAAE,cAAc,KAChB,OAAO,CAAC,eAAe,CAAC,CAAC;AAI9B,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5C,oEAAoE;IACpE,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,GAAE,gBAAqB,GAC7B,iBAAiB,CAoDnB"}
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ // @typokit/platform-bun — Bun Platform Adapter
2
+ /** Returns Bun platform info */
3
+ export function getPlatformInfo() {
4
+ const bun = globalThis.Bun;
5
+ return {
6
+ runtime: "bun",
7
+ version: bun?.version ?? "unknown",
8
+ };
9
+ }
10
+ // ─── Request / Response Helpers ──────────────────────────────
11
+ /** Parse query string from a URL into a Record */
12
+ function parseQuery(searchParams) {
13
+ const result = {};
14
+ for (const [key, value] of searchParams.entries()) {
15
+ const existing = result[key];
16
+ if (existing === undefined) {
17
+ result[key] = value;
18
+ }
19
+ else if (Array.isArray(existing)) {
20
+ existing.push(value);
21
+ }
22
+ else {
23
+ result[key] = [existing, value];
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ /** Normalize Web API headers into a flat Record */
29
+ function normalizeHeaders(headers) {
30
+ const result = {};
31
+ headers.forEach((value, key) => {
32
+ result[key] = value;
33
+ });
34
+ return result;
35
+ }
36
+ /**
37
+ * Normalize a Web API Request (used by Bun.serve) into a TypoKitRequest.
38
+ */
39
+ export async function normalizeRequest(req) {
40
+ const url = new URL(req.url);
41
+ let body = undefined;
42
+ if (req.method !== "GET" && req.method !== "HEAD") {
43
+ const contentType = req.headers.get("content-type") ?? "";
44
+ const raw = await req.text();
45
+ if (raw) {
46
+ if (contentType.includes("application/json")) {
47
+ try {
48
+ body = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ body = raw;
52
+ }
53
+ }
54
+ else {
55
+ body = raw;
56
+ }
57
+ }
58
+ }
59
+ return {
60
+ method: req.method.toUpperCase(),
61
+ path: url.pathname,
62
+ headers: normalizeHeaders(req.headers),
63
+ body,
64
+ query: parseQuery(url.searchParams),
65
+ params: {},
66
+ };
67
+ }
68
+ /**
69
+ * Convert a TypoKitResponse into a Web API Response for Bun.serve().
70
+ */
71
+ export function buildResponse(response) {
72
+ const headers = new Headers();
73
+ for (const [key, value] of Object.entries(response.headers)) {
74
+ if (value !== undefined) {
75
+ if (Array.isArray(value)) {
76
+ for (const v of value) {
77
+ headers.append(key, v);
78
+ }
79
+ }
80
+ else {
81
+ headers.set(key, value);
82
+ }
83
+ }
84
+ }
85
+ let bodyContent = null;
86
+ if (response.body === null || response.body === undefined) {
87
+ bodyContent = null;
88
+ }
89
+ else if (typeof response.body === "string") {
90
+ bodyContent = response.body;
91
+ }
92
+ else {
93
+ if (!headers.has("content-type")) {
94
+ headers.set("content-type", "application/json");
95
+ }
96
+ bodyContent = JSON.stringify(response.body);
97
+ }
98
+ return new Response(bodyContent, {
99
+ status: response.status,
100
+ headers,
101
+ });
102
+ }
103
+ /**
104
+ * Create a Bun HTTP server that dispatches to a TypoKit request handler.
105
+ *
106
+ * Usage:
107
+ * ```ts
108
+ * const srv = createServer(async (req) => ({
109
+ * status: 200,
110
+ * headers: {},
111
+ * body: { ok: true },
112
+ * }));
113
+ * const handle = await srv.listen(3000);
114
+ * // ... later
115
+ * await handle.close();
116
+ * ```
117
+ */
118
+ export function createServer(handler, options = {}) {
119
+ const hostname = options.hostname ?? "0.0.0.0";
120
+ let bunServer = null;
121
+ const instance = {
122
+ get server() {
123
+ return bunServer;
124
+ },
125
+ listen(port) {
126
+ return new Promise((resolve, reject) => {
127
+ try {
128
+ const bun = globalThis.Bun;
129
+ bunServer = bun.serve({
130
+ port,
131
+ hostname,
132
+ async fetch(req) {
133
+ try {
134
+ const normalized = await normalizeRequest(req);
135
+ const response = await handler(normalized);
136
+ return buildResponse(response);
137
+ }
138
+ catch (err) {
139
+ return new Response(JSON.stringify({
140
+ error: "Internal Server Error",
141
+ message: err instanceof Error ? err.message : "Unknown error",
142
+ }), {
143
+ status: 500,
144
+ headers: { "content-type": "application/json" },
145
+ });
146
+ }
147
+ },
148
+ });
149
+ resolve({
150
+ async close() {
151
+ if (bunServer) {
152
+ bunServer.stop(true);
153
+ bunServer = null;
154
+ }
155
+ },
156
+ });
157
+ }
158
+ catch (err) {
159
+ reject(err);
160
+ }
161
+ });
162
+ },
163
+ };
164
+ return instance;
165
+ }
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@typokit/platform-bun",
3
+ "exports": {
4
+ ".": {
5
+ "import": "./dist/index.js",
6
+ "types": "./dist/index.d.ts"
7
+ }
8
+ },
9
+ "version": "0.1.4",
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "dependencies": {
18
+ "@typokit/types": "0.1.4"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/KyleBastien/typokit",
23
+ "directory": "packages/platform-bun"
24
+ },
25
+ "scripts": {
26
+ "test": "rstest run --passWithNoTests"
27
+ }
28
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ // Minimal Web API type declarations for Bun platform adapter.
2
+ // These types are available in Bun natively, but we declare them here
3
+ // so the package compiles under Node16 moduleResolution without DOM lib.
4
+
5
+ declare class URL {
6
+ constructor(url: string, base?: string);
7
+ readonly pathname: string;
8
+ readonly searchParams: URLSearchParams;
9
+ }
10
+
11
+ declare class URLSearchParams {
12
+ entries(): IterableIterator<[string, string]>;
13
+ }
14
+
15
+ declare class Headers {
16
+ constructor();
17
+ get(name: string): string | null;
18
+ has(name: string): boolean;
19
+ set(name: string, value: string): void;
20
+ append(name: string, value: string): void;
21
+ forEach(callback: (value: string, key: string) => void): void;
22
+ }
23
+
24
+ declare class Request {
25
+ constructor(input: string, init?: RequestInit);
26
+ readonly url: string;
27
+ readonly method: string;
28
+ readonly headers: Headers;
29
+ text(): Promise<string>;
30
+ json(): Promise<unknown>;
31
+ }
32
+
33
+ interface RequestInit {
34
+ method?: string;
35
+ headers?: Record<string, string> | Headers;
36
+ body?: string | null;
37
+ }
38
+
39
+ declare class Response {
40
+ constructor(body?: string | null, init?: ResponseInit);
41
+ readonly status: number;
42
+ readonly headers: Headers;
43
+ text(): Promise<string>;
44
+ json(): Promise<unknown>;
45
+ }
46
+
47
+ interface ResponseInit {
48
+ status?: number;
49
+ headers?: Record<string, string> | Headers;
50
+ }
@@ -0,0 +1,289 @@
1
+ // @typokit/platform-bun — Tests
2
+
3
+ import { describe, it, expect } from "@rstest/core";
4
+ import {
5
+ normalizeRequest,
6
+ buildResponse,
7
+ getPlatformInfo,
8
+ createServer,
9
+ } from "./index.js";
10
+ import type { TypoKitResponse } from "@typokit/types";
11
+
12
+ // ─── getPlatformInfo ─────────────────────────────────────────
13
+
14
+ describe("getPlatformInfo", () => {
15
+ it("returns bun runtime", () => {
16
+ // Mock Bun global for testing in Node environment
17
+ const g = globalThis as unknown as Record<string, unknown>;
18
+ g["Bun"] = { version: "1.0.0" };
19
+ try {
20
+ const info = getPlatformInfo();
21
+ expect(info.runtime).toBe("bun");
22
+ expect(info.version).toBe("1.0.0");
23
+ } finally {
24
+ delete g["Bun"];
25
+ }
26
+ });
27
+
28
+ it("returns unknown version when Bun global is missing", () => {
29
+ const info = getPlatformInfo();
30
+ expect(info.runtime).toBe("bun");
31
+ expect(info.version).toBe("unknown");
32
+ });
33
+ });
34
+
35
+ // ─── normalizeRequest ────────────────────────────────────────
36
+
37
+ describe("normalizeRequest", () => {
38
+ it("parses method, path, headers, and query from Request", async () => {
39
+ const req = new Request("http://localhost:3000/hello?foo=bar", {
40
+ method: "GET",
41
+ headers: { "x-test": "yes" },
42
+ });
43
+
44
+ const normalized = await normalizeRequest(req);
45
+
46
+ expect(normalized.method).toBe("GET");
47
+ expect(normalized.path).toBe("/hello");
48
+ expect(normalized.query["foo"]).toBe("bar");
49
+ expect(normalized.headers["x-test"]).toBe("yes");
50
+ expect(normalized.params).toEqual({});
51
+ });
52
+
53
+ it("collects JSON body when content-type is application/json", async () => {
54
+ const req = new Request("http://localhost:3000/data", {
55
+ method: "POST",
56
+ headers: { "content-type": "application/json" },
57
+ body: JSON.stringify({ name: "test" }),
58
+ });
59
+
60
+ const normalized = await normalizeRequest(req);
61
+
62
+ expect(normalized.method).toBe("POST");
63
+ expect(normalized.body).toEqual({ name: "test" });
64
+ });
65
+
66
+ it("returns raw string body when content-type is not JSON", async () => {
67
+ const req = new Request("http://localhost:3000/text", {
68
+ method: "POST",
69
+ headers: { "content-type": "text/plain" },
70
+ body: "hello world",
71
+ });
72
+
73
+ const normalized = await normalizeRequest(req);
74
+ expect(normalized.body).toBe("hello world");
75
+ });
76
+
77
+ it("returns undefined body for GET requests", async () => {
78
+ const req = new Request("http://localhost:3000/empty", {
79
+ method: "GET",
80
+ });
81
+
82
+ const normalized = await normalizeRequest(req);
83
+ expect(normalized.body).toBeUndefined();
84
+ });
85
+
86
+ it("handles multiple query params with the same key", async () => {
87
+ const req = new Request("http://localhost:3000/multi?tag=a&tag=b", {
88
+ method: "GET",
89
+ });
90
+
91
+ const normalized = await normalizeRequest(req);
92
+ expect(normalized.query["tag"]).toEqual(["a", "b"]);
93
+ });
94
+ });
95
+
96
+ // ─── buildResponse ───────────────────────────────────────────
97
+
98
+ describe("buildResponse", () => {
99
+ it("builds a Response with status, headers, and JSON body", () => {
100
+ const typoResponse: TypoKitResponse = {
101
+ status: 201,
102
+ headers: { "x-custom": "value" },
103
+ body: { created: true },
104
+ };
105
+
106
+ const response = buildResponse(typoResponse);
107
+
108
+ expect(response.status).toBe(201);
109
+ expect(response.headers.get("x-custom")).toBe("value");
110
+ expect(response.headers.get("content-type")).toBe("application/json");
111
+ });
112
+
113
+ it("builds a Response with string body", async () => {
114
+ const typoResponse: TypoKitResponse = {
115
+ status: 200,
116
+ headers: { "content-type": "text/plain" },
117
+ body: "hello",
118
+ };
119
+
120
+ const response = buildResponse(typoResponse);
121
+ const text = await response.text();
122
+
123
+ expect(response.status).toBe(200);
124
+ expect(text).toBe("hello");
125
+ });
126
+
127
+ it("builds a Response with null body", () => {
128
+ const typoResponse: TypoKitResponse = {
129
+ status: 204,
130
+ headers: {},
131
+ body: null,
132
+ };
133
+
134
+ const response = buildResponse(typoResponse);
135
+ expect(response.status).toBe(204);
136
+ });
137
+
138
+ it("handles array header values", () => {
139
+ const typoResponse: TypoKitResponse = {
140
+ status: 200,
141
+ headers: { "set-cookie": ["a=1", "b=2"] },
142
+ body: null,
143
+ };
144
+
145
+ const response = buildResponse(typoResponse);
146
+ expect(response.headers.get("set-cookie")).toContain("a=1");
147
+ });
148
+ });
149
+
150
+ // ─── createServer ────────────────────────────────────────────
151
+
152
+ describe("createServer", () => {
153
+ it("creates a server instance with listen method", () => {
154
+ const srv = createServer(async () => ({
155
+ status: 200,
156
+ headers: {},
157
+ body: null,
158
+ }));
159
+
160
+ expect(typeof srv.listen).toBe("function");
161
+ expect(srv.server).toBeNull();
162
+ });
163
+
164
+ it("calls Bun.serve when listen is invoked", async () => {
165
+ const mockServer = {
166
+ port: 3000,
167
+ hostname: "0.0.0.0",
168
+ stop: () => {},
169
+ };
170
+
171
+ const g = globalThis as unknown as Record<string, unknown>;
172
+ g["Bun"] = {
173
+ version: "1.0.0",
174
+ serve: () => mockServer,
175
+ };
176
+
177
+ try {
178
+ const srv = createServer(async () => ({
179
+ status: 200,
180
+ headers: {},
181
+ body: { ok: true },
182
+ }));
183
+
184
+ const handle = await srv.listen(3000);
185
+ expect(srv.server).not.toBeNull();
186
+
187
+ await handle.close();
188
+ expect(srv.server).toBeNull();
189
+ } finally {
190
+ delete g["Bun"];
191
+ }
192
+ });
193
+
194
+ it("rejects when Bun global is not available", async () => {
195
+ const srv = createServer(async () => ({
196
+ status: 200,
197
+ headers: {},
198
+ body: null,
199
+ }));
200
+
201
+ let error: Error | null = null;
202
+ try {
203
+ await srv.listen(3000);
204
+ } catch (err) {
205
+ error = err as Error;
206
+ }
207
+ expect(error).not.toBeNull();
208
+ });
209
+
210
+ it("fetch handler converts request and returns response", async () => {
211
+ let capturedFetch: ((req: Request) => Promise<Response>) | null = null;
212
+ const mockServer = {
213
+ port: 3001,
214
+ hostname: "0.0.0.0",
215
+ stop: () => {},
216
+ };
217
+
218
+ const g = globalThis as unknown as Record<string, unknown>;
219
+ g["Bun"] = {
220
+ version: "1.0.0",
221
+ serve: (opts: { fetch: (req: Request) => Promise<Response> }) => {
222
+ capturedFetch = opts.fetch;
223
+ return mockServer;
224
+ },
225
+ };
226
+
227
+ try {
228
+ const srv = createServer(async (req) => ({
229
+ status: 200,
230
+ headers: { "content-type": "application/json" },
231
+ body: { echo: req.path },
232
+ }));
233
+
234
+ await srv.listen(3001);
235
+
236
+ // Simulate a request through the captured fetch handler
237
+ const webReq = new Request("http://localhost:3001/test-path");
238
+ const webResp = await capturedFetch!(webReq);
239
+
240
+ expect(webResp.status).toBe(200);
241
+ const body = await webResp.json();
242
+ expect(body).toEqual({ echo: "/test-path" });
243
+
244
+ const handle = await srv.listen(3001);
245
+ await handle.close();
246
+ } finally {
247
+ delete g["Bun"];
248
+ }
249
+ });
250
+
251
+ it("fetch handler returns 500 when handler throws", async () => {
252
+ let capturedFetch: ((req: Request) => Promise<Response>) | null = null;
253
+ const mockServer = {
254
+ port: 3002,
255
+ hostname: "0.0.0.0",
256
+ stop: () => {},
257
+ };
258
+
259
+ const g = globalThis as unknown as Record<string, unknown>;
260
+ g["Bun"] = {
261
+ version: "1.0.0",
262
+ serve: (opts: { fetch: (req: Request) => Promise<Response> }) => {
263
+ capturedFetch = opts.fetch;
264
+ return mockServer;
265
+ },
266
+ };
267
+
268
+ try {
269
+ const srv = createServer(async () => {
270
+ throw new Error("boom");
271
+ });
272
+
273
+ await srv.listen(3002);
274
+
275
+ const webReq = new Request("http://localhost:3002/fail");
276
+ const webResp = await capturedFetch!(webReq);
277
+
278
+ expect(webResp.status).toBe(500);
279
+ const body = (await webResp.json()) as { error: string; message: string };
280
+ expect(body.error).toBe("Internal Server Error");
281
+ expect(body.message).toBe("boom");
282
+
283
+ const handle = await srv.listen(3002);
284
+ await handle.close();
285
+ } finally {
286
+ delete g["Bun"];
287
+ }
288
+ });
289
+ });
package/src/index.ts ADDED
@@ -0,0 +1,244 @@
1
+ // @typokit/platform-bun — Bun Platform Adapter
2
+
3
+ import type {
4
+ HttpMethod,
5
+ ServerHandle,
6
+ TypoKitRequest,
7
+ TypoKitResponse,
8
+ } from "@typokit/types";
9
+
10
+ // ─── Bun Type Declarations ──────────────────────────────────
11
+ // Minimal type declarations for Bun APIs so this package compiles
12
+ // without bun-types installed (they're only available in Bun runtimes).
13
+
14
+ /** Subset of Bun's Server type we rely on */
15
+ interface BunServer {
16
+ port: number;
17
+ hostname: string;
18
+ stop(closeActiveConnections?: boolean): void;
19
+ }
20
+
21
+ /** Options passed to Bun.serve() */
22
+ interface BunServeOptions {
23
+ port: number;
24
+ hostname: string;
25
+ fetch(req: Request): Promise<Response> | Response;
26
+ }
27
+
28
+ /** Minimal shape of the global Bun object */
29
+ interface BunGlobal {
30
+ version: string;
31
+ serve(options: BunServeOptions): BunServer;
32
+ }
33
+
34
+ // ─── Platform Info ───────────────────────────────────────────
35
+
36
+ /** Runtime platform metadata for diagnostics and inspect commands */
37
+ export interface PlatformInfo {
38
+ runtime: string;
39
+ version: string;
40
+ }
41
+
42
+ /** Returns Bun platform info */
43
+ export function getPlatformInfo(): PlatformInfo {
44
+ const bun = (globalThis as unknown as { Bun: BunGlobal }).Bun;
45
+ return {
46
+ runtime: "bun",
47
+ version: bun?.version ?? "unknown",
48
+ };
49
+ }
50
+
51
+ // ─── Request / Response Helpers ──────────────────────────────
52
+
53
+ /** Parse query string from a URL into a Record */
54
+ function parseQuery(
55
+ searchParams: URLSearchParams,
56
+ ): Record<string, string | string[] | undefined> {
57
+ const result: Record<string, string | string[] | undefined> = {};
58
+ for (const [key, value] of searchParams.entries()) {
59
+ const existing = result[key];
60
+ if (existing === undefined) {
61
+ result[key] = value;
62
+ } else if (Array.isArray(existing)) {
63
+ existing.push(value);
64
+ } else {
65
+ result[key] = [existing, value];
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+
71
+ /** Normalize Web API headers into a flat Record */
72
+ function normalizeHeaders(
73
+ headers: Headers,
74
+ ): Record<string, string | string[] | undefined> {
75
+ const result: Record<string, string | string[] | undefined> = {};
76
+ headers.forEach((value, key) => {
77
+ result[key] = value;
78
+ });
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Normalize a Web API Request (used by Bun.serve) into a TypoKitRequest.
84
+ */
85
+ export async function normalizeRequest(req: Request): Promise<TypoKitRequest> {
86
+ const url = new URL(req.url);
87
+
88
+ let body: unknown = undefined;
89
+ if (req.method !== "GET" && req.method !== "HEAD") {
90
+ const contentType = req.headers.get("content-type") ?? "";
91
+ const raw = await req.text();
92
+ if (raw) {
93
+ if (contentType.includes("application/json")) {
94
+ try {
95
+ body = JSON.parse(raw);
96
+ } catch {
97
+ body = raw;
98
+ }
99
+ } else {
100
+ body = raw;
101
+ }
102
+ }
103
+ }
104
+
105
+ return {
106
+ method: req.method.toUpperCase() as HttpMethod,
107
+ path: url.pathname,
108
+ headers: normalizeHeaders(req.headers),
109
+ body,
110
+ query: parseQuery(url.searchParams),
111
+ params: {},
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Convert a TypoKitResponse into a Web API Response for Bun.serve().
117
+ */
118
+ export function buildResponse(response: TypoKitResponse): Response {
119
+ const headers = new Headers();
120
+ for (const [key, value] of Object.entries(response.headers)) {
121
+ if (value !== undefined) {
122
+ if (Array.isArray(value)) {
123
+ for (const v of value) {
124
+ headers.append(key, v);
125
+ }
126
+ } else {
127
+ headers.set(key, value);
128
+ }
129
+ }
130
+ }
131
+
132
+ let bodyContent: string | null = null;
133
+ if (response.body === null || response.body === undefined) {
134
+ bodyContent = null;
135
+ } else if (typeof response.body === "string") {
136
+ bodyContent = response.body;
137
+ } else {
138
+ if (!headers.has("content-type")) {
139
+ headers.set("content-type", "application/json");
140
+ }
141
+ bodyContent = JSON.stringify(response.body);
142
+ }
143
+
144
+ return new Response(bodyContent, {
145
+ status: response.status,
146
+ headers,
147
+ });
148
+ }
149
+
150
+ // ─── Request Handler Type ────────────────────────────────────
151
+
152
+ /** Handler function that receives a normalized request and returns a response */
153
+ export type BunRequestHandler = (
154
+ req: TypoKitRequest,
155
+ ) => Promise<TypoKitResponse>;
156
+
157
+ // ─── Bun Server Options ─────────────────────────────────────
158
+
159
+ export interface BunServerOptions {
160
+ /** Optional hostname to bind to (default: "0.0.0.0") */
161
+ hostname?: string;
162
+ }
163
+
164
+ // ─── Bun Server ─────────────────────────────────────────────
165
+
166
+ /** Result of createServer — provides listen/close and access to the underlying Bun server */
167
+ export interface BunServerInstance {
168
+ /** Start listening on the given port. Returns a handle for graceful shutdown. */
169
+ listen(port: number): Promise<ServerHandle>;
170
+ /** The underlying Bun server instance (available after listen()) */
171
+ server: BunServer | null;
172
+ }
173
+
174
+ /**
175
+ * Create a Bun HTTP server that dispatches to a TypoKit request handler.
176
+ *
177
+ * Usage:
178
+ * ```ts
179
+ * const srv = createServer(async (req) => ({
180
+ * status: 200,
181
+ * headers: {},
182
+ * body: { ok: true },
183
+ * }));
184
+ * const handle = await srv.listen(3000);
185
+ * // ... later
186
+ * await handle.close();
187
+ * ```
188
+ */
189
+ export function createServer(
190
+ handler: BunRequestHandler,
191
+ options: BunServerOptions = {},
192
+ ): BunServerInstance {
193
+ const hostname = options.hostname ?? "0.0.0.0";
194
+ let bunServer: BunServer | null = null;
195
+
196
+ const instance: BunServerInstance = {
197
+ get server(): BunServer | null {
198
+ return bunServer;
199
+ },
200
+ listen(port: number): Promise<ServerHandle> {
201
+ return new Promise((resolve, reject) => {
202
+ try {
203
+ const bun = (globalThis as unknown as { Bun: BunGlobal }).Bun;
204
+ bunServer = bun.serve({
205
+ port,
206
+ hostname,
207
+ async fetch(req: Request): Promise<Response> {
208
+ try {
209
+ const normalized = await normalizeRequest(req);
210
+ const response = await handler(normalized);
211
+ return buildResponse(response);
212
+ } catch (err: unknown) {
213
+ return new Response(
214
+ JSON.stringify({
215
+ error: "Internal Server Error",
216
+ message:
217
+ err instanceof Error ? err.message : "Unknown error",
218
+ }),
219
+ {
220
+ status: 500,
221
+ headers: { "content-type": "application/json" },
222
+ },
223
+ );
224
+ }
225
+ },
226
+ });
227
+
228
+ resolve({
229
+ async close(): Promise<void> {
230
+ if (bunServer) {
231
+ bunServer.stop(true);
232
+ bunServer = null;
233
+ }
234
+ },
235
+ });
236
+ } catch (err) {
237
+ reject(err);
238
+ }
239
+ });
240
+ },
241
+ };
242
+
243
+ return instance;
244
+ }