@alt-stack/server-bun 0.4.5
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/LICENSE +21 -0
- package/package.json +33 -0
- package/src/docs.ts +115 -0
- package/src/index.ts +142 -0
- package/src/router.ts +100 -0
- package/src/server.spec.ts +458 -0
- package/src/server.ts +527 -0
- package/src/types.spec.ts +40 -0
- package/src/types.ts +18 -0
- package/tsconfig.json +12 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { ZodError } from "zod";
|
|
3
|
+
import type {
|
|
4
|
+
TypedContext,
|
|
5
|
+
InputConfig,
|
|
6
|
+
TelemetryOption,
|
|
7
|
+
} from "@alt-stack/server-core";
|
|
8
|
+
import type { Procedure } from "@alt-stack/server-core";
|
|
9
|
+
import type { Router } from "@alt-stack/server-core";
|
|
10
|
+
import {
|
|
11
|
+
validateInput,
|
|
12
|
+
middlewareMarker,
|
|
13
|
+
middlewareOk,
|
|
14
|
+
resolveTelemetryConfig,
|
|
15
|
+
shouldIgnoreRoute,
|
|
16
|
+
initTelemetry,
|
|
17
|
+
createRequestSpan,
|
|
18
|
+
endSpanWithError,
|
|
19
|
+
setSpanOk,
|
|
20
|
+
withActiveSpan,
|
|
21
|
+
isErr,
|
|
22
|
+
ok as resultOk,
|
|
23
|
+
err as resultErr,
|
|
24
|
+
findHttpStatusForError,
|
|
25
|
+
} from "@alt-stack/server-core";
|
|
26
|
+
import type {
|
|
27
|
+
MiddlewareResult,
|
|
28
|
+
MiddlewareResultSuccess,
|
|
29
|
+
} from "@alt-stack/server-core";
|
|
30
|
+
import { BunRouter } from "./router.ts";
|
|
31
|
+
import type { BunServer, BunBaseContext } from "./types.ts";
|
|
32
|
+
|
|
33
|
+
function normalizePrefix(prefix: string): string {
|
|
34
|
+
const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
35
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizePath(prefix: string, path: string): string {
|
|
39
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
40
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
41
|
+
const cleanPath =
|
|
42
|
+
normalizedPath.endsWith("/") && normalizedPath !== "/"
|
|
43
|
+
? normalizedPath.slice(0, -1)
|
|
44
|
+
: normalizedPath;
|
|
45
|
+
if (cleanPath === "/") {
|
|
46
|
+
return normalizedPrefix;
|
|
47
|
+
}
|
|
48
|
+
return `${normalizedPrefix}${cleanPath}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Serialize a ResultError for JSON response.
|
|
53
|
+
* Extracts error properties beyond the base Error fields.
|
|
54
|
+
*/
|
|
55
|
+
function serializeError(error: Error & { _tag: string }): object {
|
|
56
|
+
const props: Record<string, unknown> = {};
|
|
57
|
+
for (const key of Object.keys(error)) {
|
|
58
|
+
if (key !== "name" && key !== "message" && key !== "stack") {
|
|
59
|
+
props[key] = (error as any)[key];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
error: {
|
|
65
|
+
code: error._tag,
|
|
66
|
+
message: error.message,
|
|
67
|
+
...props,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a JSON response
|
|
74
|
+
*/
|
|
75
|
+
function jsonResponse(data: unknown, status: number = 200): Response {
|
|
76
|
+
return new Response(JSON.stringify(data), {
|
|
77
|
+
status,
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createServer<
|
|
83
|
+
TContext extends BunBaseContext = BunBaseContext,
|
|
84
|
+
>(
|
|
85
|
+
config: Record<string, Router<TContext> | Router<TContext>[]>,
|
|
86
|
+
options?: {
|
|
87
|
+
createContext?: (
|
|
88
|
+
req: Request,
|
|
89
|
+
server: BunServer,
|
|
90
|
+
) => Promise<Omit<TContext, "bun" | "span">> | Omit<TContext, "bun" | "span">;
|
|
91
|
+
defaultErrorHandlers?: {
|
|
92
|
+
default400Error: (
|
|
93
|
+
errors: Array<
|
|
94
|
+
[error: ZodError, variant: "body" | "param" | "query", value: unknown]
|
|
95
|
+
>,
|
|
96
|
+
) => [z.ZodObject<any>, z.infer<z.ZodObject<any>>];
|
|
97
|
+
default500Error: (
|
|
98
|
+
error: unknown,
|
|
99
|
+
) => [z.ZodObject<any>, z.infer<z.ZodObject<any>>];
|
|
100
|
+
default400ErrorSchema?: z.ZodObject<any>;
|
|
101
|
+
default500ErrorSchema?: z.ZodObject<any>;
|
|
102
|
+
};
|
|
103
|
+
telemetry?: TelemetryOption;
|
|
104
|
+
port?: number;
|
|
105
|
+
hostname?: string;
|
|
106
|
+
},
|
|
107
|
+
): BunServer {
|
|
108
|
+
const bunRouter = new BunRouter();
|
|
109
|
+
const telemetryConfig = resolveTelemetryConfig(options?.telemetry);
|
|
110
|
+
|
|
111
|
+
// Initialize telemetry if enabled
|
|
112
|
+
if (telemetryConfig.enabled) {
|
|
113
|
+
initTelemetry();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Collect all procedures from all routers
|
|
117
|
+
const procedures: Procedure<
|
|
118
|
+
InputConfig,
|
|
119
|
+
z.ZodTypeAny | undefined,
|
|
120
|
+
Record<number, z.ZodTypeAny> | undefined,
|
|
121
|
+
TContext
|
|
122
|
+
>[] = [];
|
|
123
|
+
|
|
124
|
+
for (const [prefix, routerOrRouters] of Object.entries(config)) {
|
|
125
|
+
const routers = Array.isArray(routerOrRouters)
|
|
126
|
+
? routerOrRouters
|
|
127
|
+
: [routerOrRouters];
|
|
128
|
+
|
|
129
|
+
for (const router of routers) {
|
|
130
|
+
const routerProcedures = router.getProcedures();
|
|
131
|
+
|
|
132
|
+
for (const procedure of routerProcedures) {
|
|
133
|
+
procedures.push({
|
|
134
|
+
...procedure,
|
|
135
|
+
path: normalizePath(prefix, procedure.path),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Register all procedures with the router
|
|
142
|
+
for (const procedure of procedures) {
|
|
143
|
+
bunRouter.register(
|
|
144
|
+
procedure.method,
|
|
145
|
+
procedure.path,
|
|
146
|
+
async (req, params, server) => {
|
|
147
|
+
// Create telemetry span if enabled
|
|
148
|
+
const url = new URL(req.url);
|
|
149
|
+
const shouldTrace =
|
|
150
|
+
telemetryConfig.enabled &&
|
|
151
|
+
!shouldIgnoreRoute(procedure.path, telemetryConfig);
|
|
152
|
+
const span = shouldTrace
|
|
153
|
+
? createRequestSpan(
|
|
154
|
+
procedure.method,
|
|
155
|
+
procedure.path,
|
|
156
|
+
url.pathname,
|
|
157
|
+
telemetryConfig,
|
|
158
|
+
)
|
|
159
|
+
: undefined;
|
|
160
|
+
|
|
161
|
+
return withActiveSpan(span, async () => {
|
|
162
|
+
try {
|
|
163
|
+
// Extract query parameters
|
|
164
|
+
const query: Record<string, unknown> = {};
|
|
165
|
+
for (const [key, value] of url.searchParams) {
|
|
166
|
+
query[key] = value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Parse body (JSON only)
|
|
170
|
+
const body = await req.json().catch(() => ({}));
|
|
171
|
+
|
|
172
|
+
const inputConfig = procedure.config.input;
|
|
173
|
+
const validatedInput = await validateInput(
|
|
174
|
+
inputConfig,
|
|
175
|
+
params,
|
|
176
|
+
query,
|
|
177
|
+
body,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const customContext = options?.createContext
|
|
181
|
+
? await options.createContext(req, server)
|
|
182
|
+
: ({} as Omit<TContext, "bun" | "span">);
|
|
183
|
+
|
|
184
|
+
type ProcedureContext = TypedContext<
|
|
185
|
+
InputConfig,
|
|
186
|
+
Record<number, z.ZodTypeAny> | undefined,
|
|
187
|
+
TContext
|
|
188
|
+
>;
|
|
189
|
+
|
|
190
|
+
const ctx: ProcedureContext = {
|
|
191
|
+
...customContext,
|
|
192
|
+
bun: { req, server },
|
|
193
|
+
input: validatedInput,
|
|
194
|
+
span,
|
|
195
|
+
} as ProcedureContext;
|
|
196
|
+
|
|
197
|
+
let currentCtx: ProcedureContext = ctx;
|
|
198
|
+
let middlewareIndex = 0;
|
|
199
|
+
|
|
200
|
+
// Get the flags for which middleware return Result types
|
|
201
|
+
const middlewareWithErrorsFlags = (procedure as any)
|
|
202
|
+
.middlewareWithErrorsFlags as boolean[] | undefined;
|
|
203
|
+
|
|
204
|
+
type MiddlewareRunResult =
|
|
205
|
+
| { ok: true; ctx: ProcedureContext }
|
|
206
|
+
| { ok: false; error: Error & { _tag: string } }
|
|
207
|
+
| { ok: true; response: Response };
|
|
208
|
+
|
|
209
|
+
const runMiddleware = async (): Promise<MiddlewareRunResult> => {
|
|
210
|
+
if (middlewareIndex >= procedure.middleware.length) {
|
|
211
|
+
return { ok: true, ctx: currentCtx };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const currentIndex = middlewareIndex;
|
|
215
|
+
const middleware = procedure.middleware[middlewareIndex++];
|
|
216
|
+
if (!middleware) {
|
|
217
|
+
return { ok: true, ctx: currentCtx };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if this middleware returns Result types
|
|
221
|
+
const isResultMiddleware =
|
|
222
|
+
middlewareWithErrorsFlags?.[currentIndex] ?? false;
|
|
223
|
+
|
|
224
|
+
if (isResultMiddleware) {
|
|
225
|
+
// Result-based middleware - provide next() that returns Result
|
|
226
|
+
const nextFn = async (opts?: {
|
|
227
|
+
ctx?: Partial<ProcedureContext>;
|
|
228
|
+
}) => {
|
|
229
|
+
if (opts?.ctx) {
|
|
230
|
+
currentCtx = { ...currentCtx, ...opts.ctx } as ProcedureContext;
|
|
231
|
+
}
|
|
232
|
+
const nextResult = await runMiddleware();
|
|
233
|
+
|
|
234
|
+
// Propagate errors from downstream middleware
|
|
235
|
+
if (!nextResult.ok) {
|
|
236
|
+
return resultErr(nextResult.error);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Handle Response objects
|
|
240
|
+
if ("response" in nextResult) {
|
|
241
|
+
return middlewareOk(currentCtx);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return middlewareOk(nextResult.ctx);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = await (middleware as any)({
|
|
248
|
+
ctx: currentCtx,
|
|
249
|
+
next: nextFn,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Result-based middleware returns Result<MiddlewareResultSuccess, Error>
|
|
253
|
+
if (result && typeof result === "object" && "_tag" in result) {
|
|
254
|
+
if (result._tag === "Err") {
|
|
255
|
+
const error = result.error as Error & { _tag: string };
|
|
256
|
+
return { ok: false, error };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (result._tag === "Ok") {
|
|
260
|
+
const value = result.value as MiddlewareResultSuccess<any>;
|
|
261
|
+
if (value && value.marker === middlewareMarker) {
|
|
262
|
+
currentCtx = {
|
|
263
|
+
...currentCtx,
|
|
264
|
+
...value.ctx,
|
|
265
|
+
} as ProcedureContext;
|
|
266
|
+
}
|
|
267
|
+
return { ok: true, ctx: currentCtx };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { ok: true, ctx: currentCtx };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Legacy middleware - throws on error, returns MiddlewareResult
|
|
275
|
+
let result: unknown;
|
|
276
|
+
try {
|
|
277
|
+
result = await middleware({
|
|
278
|
+
ctx: currentCtx,
|
|
279
|
+
next: async (
|
|
280
|
+
opts?: { ctx?: Partial<ProcedureContext> },
|
|
281
|
+
): Promise<MiddlewareResult<Partial<ProcedureContext>>> => {
|
|
282
|
+
// Merge context updates if provided
|
|
283
|
+
if (opts?.ctx) {
|
|
284
|
+
currentCtx = { ...currentCtx, ...opts.ctx } as ProcedureContext;
|
|
285
|
+
}
|
|
286
|
+
const nextResult = await runMiddleware();
|
|
287
|
+
|
|
288
|
+
// Propagate errors from Result-based middleware
|
|
289
|
+
if (!nextResult.ok) {
|
|
290
|
+
throw nextResult.error;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Handle Response objects
|
|
294
|
+
if ("response" in nextResult) {
|
|
295
|
+
return {
|
|
296
|
+
marker: middlewareMarker,
|
|
297
|
+
ok: true as const,
|
|
298
|
+
data: nextResult.response,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
marker: middlewareMarker,
|
|
304
|
+
ok: true as const,
|
|
305
|
+
data: nextResult.ctx,
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
} catch (thrownError) {
|
|
310
|
+
// Check if this is a propagated middleware error with _tag
|
|
311
|
+
if (
|
|
312
|
+
thrownError &&
|
|
313
|
+
thrownError instanceof Error &&
|
|
314
|
+
typeof (thrownError as any)._tag === "string"
|
|
315
|
+
) {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
error: thrownError as Error & { _tag: string },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// Re-throw other errors to be handled by outer try-catch
|
|
322
|
+
throw thrownError;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Handle both legacy middleware (returns context/Response) and new middleware (returns MiddlewareResult)
|
|
326
|
+
if (result instanceof Response) {
|
|
327
|
+
return { ok: true, response: result };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if middleware returned a Result type (err() call)
|
|
331
|
+
if (result && typeof result === "object" && "_tag" in result) {
|
|
332
|
+
const resultWithTag = result as {
|
|
333
|
+
_tag: string;
|
|
334
|
+
error?: unknown;
|
|
335
|
+
value?: unknown;
|
|
336
|
+
};
|
|
337
|
+
if (resultWithTag._tag === "Err") {
|
|
338
|
+
const error = resultWithTag.error as Error & { _tag: string };
|
|
339
|
+
return { ok: false, error };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (resultWithTag._tag === "Ok") {
|
|
343
|
+
const value = resultWithTag.value as MiddlewareResultSuccess<any>;
|
|
344
|
+
if (value && value.marker === middlewareMarker) {
|
|
345
|
+
currentCtx = {
|
|
346
|
+
...currentCtx,
|
|
347
|
+
...value.ctx,
|
|
348
|
+
} as ProcedureContext;
|
|
349
|
+
}
|
|
350
|
+
return { ok: true, ctx: currentCtx };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check if it's a MiddlewareResult wrapper
|
|
355
|
+
if (
|
|
356
|
+
result &&
|
|
357
|
+
typeof result === "object" &&
|
|
358
|
+
"marker" in result &&
|
|
359
|
+
"ok" in result
|
|
360
|
+
) {
|
|
361
|
+
const data = (result as any).data;
|
|
362
|
+
if (data instanceof Response) {
|
|
363
|
+
return { ok: true, response: data };
|
|
364
|
+
}
|
|
365
|
+
currentCtx = data as ProcedureContext;
|
|
366
|
+
return { ok: true, ctx: currentCtx };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Legacy middleware - direct context return
|
|
370
|
+
currentCtx = result as ProcedureContext;
|
|
371
|
+
return { ok: true, ctx: currentCtx };
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const middlewareResult = await runMiddleware();
|
|
375
|
+
|
|
376
|
+
// Handle middleware errors (from Result-based middleware)
|
|
377
|
+
if (!middlewareResult.ok) {
|
|
378
|
+
const error = middlewareResult.error as Error & { _tag: string };
|
|
379
|
+
const statusCode = findHttpStatusForError(
|
|
380
|
+
error._tag,
|
|
381
|
+
procedure.config.errors as any,
|
|
382
|
+
);
|
|
383
|
+
const errorData = serializeError(error);
|
|
384
|
+
|
|
385
|
+
span?.setAttribute("http.response.status_code", statusCode);
|
|
386
|
+
span?.end();
|
|
387
|
+
return jsonResponse(errorData, statusCode);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Handle Response objects from middleware
|
|
391
|
+
if ("response" in middlewareResult) {
|
|
392
|
+
span?.setAttribute(
|
|
393
|
+
"http.response.status_code",
|
|
394
|
+
middlewareResult.response.status,
|
|
395
|
+
);
|
|
396
|
+
setSpanOk(span);
|
|
397
|
+
span?.end();
|
|
398
|
+
return middlewareResult.response;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
currentCtx = middlewareResult.ctx;
|
|
402
|
+
|
|
403
|
+
const result = await procedure.handler(currentCtx);
|
|
404
|
+
|
|
405
|
+
// Handle Result type - check if it's Ok or Err
|
|
406
|
+
if (isErr(result)) {
|
|
407
|
+
const error = result.error;
|
|
408
|
+
const statusCode = findHttpStatusForError(
|
|
409
|
+
error._tag,
|
|
410
|
+
procedure.config.errors as any,
|
|
411
|
+
);
|
|
412
|
+
const errorData = serializeError(error);
|
|
413
|
+
|
|
414
|
+
span?.setAttribute("http.response.status_code", statusCode);
|
|
415
|
+
span?.end();
|
|
416
|
+
return jsonResponse(errorData, statusCode);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// It's an Ok result
|
|
420
|
+
const response = result.value;
|
|
421
|
+
|
|
422
|
+
// If handler returns a Response directly, return it as-is
|
|
423
|
+
if (response instanceof Response) {
|
|
424
|
+
span?.setAttribute("http.response.status_code", response.status);
|
|
425
|
+
setSpanOk(span);
|
|
426
|
+
span?.end();
|
|
427
|
+
return response;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (procedure.config.output) {
|
|
431
|
+
const validated = procedure.config.output.parse(response);
|
|
432
|
+
span?.setAttribute("http.response.status_code", 200);
|
|
433
|
+
setSpanOk(span);
|
|
434
|
+
span?.end();
|
|
435
|
+
return jsonResponse(validated);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
span?.setAttribute("http.response.status_code", 200);
|
|
439
|
+
setSpanOk(span);
|
|
440
|
+
span?.end();
|
|
441
|
+
return jsonResponse(response);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
endSpanWithError(span, error);
|
|
444
|
+
// Check for validation errors (thrown by internal validateInput)
|
|
445
|
+
if (error instanceof Error && error.name === "ValidationError") {
|
|
446
|
+
const validationError = error as Error & { details?: unknown };
|
|
447
|
+
span?.setAttribute("http.response.status_code", 400);
|
|
448
|
+
span?.end();
|
|
449
|
+
// Use default 400 error handler if available
|
|
450
|
+
if (
|
|
451
|
+
options?.defaultErrorHandlers &&
|
|
452
|
+
validationError.details &&
|
|
453
|
+
typeof validationError.details === "object" &&
|
|
454
|
+
"errors" in validationError.details &&
|
|
455
|
+
Array.isArray(
|
|
456
|
+
(validationError.details as { errors?: unknown }).errors,
|
|
457
|
+
)
|
|
458
|
+
) {
|
|
459
|
+
const errors = (
|
|
460
|
+
validationError.details as { errors: unknown[] }
|
|
461
|
+
).errors as Array<
|
|
462
|
+
[ZodError, "body" | "param" | "query", unknown]
|
|
463
|
+
>;
|
|
464
|
+
const [_schema, instance] =
|
|
465
|
+
options.defaultErrorHandlers.default400Error(errors);
|
|
466
|
+
return jsonResponse({ error: instance }, 400);
|
|
467
|
+
}
|
|
468
|
+
// Fallback to default validation error format
|
|
469
|
+
return jsonResponse(
|
|
470
|
+
{
|
|
471
|
+
error: {
|
|
472
|
+
code: "VALIDATION_ERROR",
|
|
473
|
+
message: validationError.message,
|
|
474
|
+
details: validationError.details
|
|
475
|
+
? Array.isArray(validationError.details)
|
|
476
|
+
? validationError.details
|
|
477
|
+
: [String(validationError.details)]
|
|
478
|
+
: [],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
400,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
span?.setAttribute("http.response.status_code", 500);
|
|
485
|
+
span?.end();
|
|
486
|
+
// Use default 500 error handler if available
|
|
487
|
+
if (options?.defaultErrorHandlers) {
|
|
488
|
+
const [_schema, instance] =
|
|
489
|
+
options.defaultErrorHandlers.default500Error(error);
|
|
490
|
+
return jsonResponse({ error: instance }, 500);
|
|
491
|
+
}
|
|
492
|
+
// Fallback to default 500 error format
|
|
493
|
+
return jsonResponse(
|
|
494
|
+
{
|
|
495
|
+
error: {
|
|
496
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
497
|
+
message:
|
|
498
|
+
error instanceof Error
|
|
499
|
+
? error.message
|
|
500
|
+
: "Internal server error",
|
|
501
|
+
details:
|
|
502
|
+
error instanceof Error && error.stack ? [error.stack] : [],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
500,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Create and return Bun server
|
|
514
|
+
const server = Bun.serve({
|
|
515
|
+
port: options?.port ?? 3000,
|
|
516
|
+
hostname: options?.hostname ?? "0.0.0.0",
|
|
517
|
+
fetch: async (req, server) => {
|
|
518
|
+
const response = await bunRouter.handle(req, server);
|
|
519
|
+
if (response) {
|
|
520
|
+
return response;
|
|
521
|
+
}
|
|
522
|
+
return jsonResponse({ error: { code: "NOT_FOUND", message: "Not Found" } }, 404);
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return server;
|
|
527
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import type { BaseContext } from "@alt-stack/server-core";
|
|
3
|
+
import type { BunBaseContext, BunServer } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
describe("Bun Types", () => {
|
|
6
|
+
describe("BunBaseContext", () => {
|
|
7
|
+
it("should extend BaseContext", () => {
|
|
8
|
+
// Type-level test: BunBaseContext should be assignable to BaseContext
|
|
9
|
+
const ctx: BunBaseContext = {
|
|
10
|
+
bun: { req: new Request("http://localhost"), server: {} as BunServer },
|
|
11
|
+
};
|
|
12
|
+
const _baseCtx: BaseContext = ctx;
|
|
13
|
+
expect(ctx.bun).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should have bun property with req and server", () => {
|
|
17
|
+
const ctx: BunBaseContext = {
|
|
18
|
+
bun: { req: new Request("http://localhost"), server: {} as BunServer },
|
|
19
|
+
};
|
|
20
|
+
expect(ctx.bun.req).toBeInstanceOf(Request);
|
|
21
|
+
expect(ctx.bun.server).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should allow custom properties alongside bun", () => {
|
|
25
|
+
interface AppContext extends BunBaseContext {
|
|
26
|
+
user: { id: string } | null;
|
|
27
|
+
requestId: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ctx: AppContext = {
|
|
31
|
+
bun: { req: new Request("http://localhost"), server: {} as BunServer },
|
|
32
|
+
user: { id: "123" },
|
|
33
|
+
requestId: "req-456",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(ctx.user?.id).toBe("123");
|
|
37
|
+
expect(ctx.requestId).toBe("req-456");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BaseContext } from "@alt-stack/server-core";
|
|
2
|
+
import type { Server } from "bun";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bun Server type without WebSocket support.
|
|
6
|
+
*/
|
|
7
|
+
export type BunServer = Server<undefined>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bun-specific base context that includes the native Request and Server objects.
|
|
11
|
+
* Extends the framework-agnostic BaseContext from server-core.
|
|
12
|
+
*/
|
|
13
|
+
export interface BunBaseContext extends BaseContext {
|
|
14
|
+
bun: {
|
|
15
|
+
req: Request;
|
|
16
|
+
server: BunServer;
|
|
17
|
+
};
|
|
18
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"extends": "../typescript-config/base.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"types": ["bun-types"],
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"noEmit": true
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*"],
|
|
11
|
+
"exclude": ["node_modules"]
|
|
12
|
+
}
|