bxo 0.0.5-dev.3 → 0.0.5-dev.31
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 +76 -1
- package/index.ts +588 -201
- package/package.json +1 -1
- package/plugins/index.ts +0 -2
- package/example.ts +0 -183
- package/plugins/auth.ts +0 -119
- package/plugins/logger.ts +0 -109
package/index.ts
CHANGED
@@ -3,33 +3,94 @@ import { z } from 'zod';
|
|
3
3
|
// Type utilities for extracting types from Zod schemas
|
4
4
|
type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
|
5
5
|
|
6
|
+
// Response configuration types
|
7
|
+
type ResponseSchema = z.ZodSchema<any>;
|
8
|
+
type StatusResponseSchema = Record<number, ResponseSchema>;
|
9
|
+
type ResponseConfig = ResponseSchema | StatusResponseSchema;
|
10
|
+
|
11
|
+
// Type utility to extract response type from response config
|
12
|
+
type InferResponseType<T> = T extends ResponseSchema
|
13
|
+
? InferZodType<T>
|
14
|
+
: T extends StatusResponseSchema
|
15
|
+
? { [K in keyof T]: InferZodType<T[K]> }[keyof T]
|
16
|
+
: never;
|
17
|
+
|
18
|
+
// Cookie interface
|
19
|
+
interface Cookie {
|
20
|
+
name: string;
|
21
|
+
value: string;
|
22
|
+
domain?: string;
|
23
|
+
path?: string;
|
24
|
+
expires?: Date;
|
25
|
+
maxAge?: number;
|
26
|
+
secure?: boolean;
|
27
|
+
httpOnly?: boolean;
|
28
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
29
|
+
}
|
30
|
+
|
31
|
+
// OpenAPI detail information
|
32
|
+
interface RouteDetail {
|
33
|
+
summary?: string;
|
34
|
+
description?: string;
|
35
|
+
tags?: string[];
|
36
|
+
operationId?: string;
|
37
|
+
deprecated?: boolean;
|
38
|
+
produces?: string[];
|
39
|
+
consumes?: string[];
|
40
|
+
[key: string]: any; // Allow additional OpenAPI properties
|
41
|
+
}
|
42
|
+
|
6
43
|
// Configuration interface for route handlers
|
7
44
|
interface RouteConfig {
|
8
45
|
params?: z.ZodSchema<any>;
|
9
46
|
query?: z.ZodSchema<any>;
|
10
47
|
body?: z.ZodSchema<any>;
|
11
48
|
headers?: z.ZodSchema<any>;
|
12
|
-
|
49
|
+
cookies?: z.ZodSchema<any>;
|
50
|
+
response?: ResponseConfig;
|
51
|
+
detail?: RouteDetail;
|
13
52
|
}
|
14
53
|
|
54
|
+
// Helper type to extract status codes from response config
|
55
|
+
type StatusCodes<T> = T extends Record<number, any> ? keyof T : never;
|
56
|
+
|
15
57
|
// Context type that's fully typed based on the route configuration
|
16
58
|
export type Context<TConfig extends RouteConfig = {}> = {
|
17
59
|
params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
|
18
60
|
query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
|
19
61
|
body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
|
20
62
|
headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
|
63
|
+
cookies: TConfig['cookies'] extends z.ZodSchema<any> ? InferZodType<TConfig['cookies']> : Record<string, string>;
|
64
|
+
path: string;
|
21
65
|
request: Request;
|
22
66
|
set: {
|
23
67
|
status?: number;
|
24
68
|
headers?: Record<string, string>;
|
69
|
+
cookies?: Cookie[];
|
25
70
|
};
|
26
|
-
|
27
|
-
|
71
|
+
status: <T extends number>(
|
72
|
+
code: TConfig['response'] extends StatusResponseSchema
|
73
|
+
? StatusCodes<TConfig['response']> | number
|
74
|
+
: T,
|
75
|
+
data?: TConfig['response'] extends StatusResponseSchema
|
76
|
+
? T extends keyof TConfig['response']
|
77
|
+
? InferZodType<TConfig['response'][T]>
|
78
|
+
: any
|
79
|
+
: TConfig['response'] extends ResponseSchema
|
80
|
+
? InferZodType<TConfig['response']>
|
81
|
+
: any
|
82
|
+
) => TConfig['response'] extends StatusResponseSchema
|
83
|
+
? T extends keyof TConfig['response']
|
84
|
+
? InferZodType<TConfig['response'][T]>
|
85
|
+
: any
|
86
|
+
: TConfig['response'] extends ResponseSchema
|
87
|
+
? InferZodType<TConfig['response']>
|
88
|
+
: any;
|
28
89
|
[key: string]: any;
|
29
90
|
};
|
30
91
|
|
31
|
-
// Handler function type
|
32
|
-
type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
|
92
|
+
// Handler function type with proper response typing
|
93
|
+
type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
|
33
94
|
|
34
95
|
// Route definition
|
35
96
|
interface Route {
|
@@ -39,75 +100,85 @@ interface Route {
|
|
39
100
|
config?: RouteConfig;
|
40
101
|
}
|
41
102
|
|
103
|
+
// WebSocket handler interface
|
104
|
+
interface WebSocketHandler {
|
105
|
+
onOpen?: (ws: any) => void;
|
106
|
+
onMessage?: (ws: any, message: string | Buffer) => void;
|
107
|
+
onClose?: (ws: any, code?: number, reason?: string) => void;
|
108
|
+
onError?: (ws: any, error: Error) => void;
|
109
|
+
}
|
110
|
+
|
111
|
+
// WebSocket route definition
|
112
|
+
interface WSRoute {
|
113
|
+
path: string;
|
114
|
+
handler: WebSocketHandler;
|
115
|
+
}
|
116
|
+
|
42
117
|
// Lifecycle hooks
|
43
118
|
interface LifecycleHooks {
|
44
|
-
onBeforeStart?: () => Promise<void> | void;
|
45
|
-
onAfterStart?: () => Promise<void> | void;
|
46
|
-
onBeforeStop?: () => Promise<void> | void;
|
47
|
-
onAfterStop?: () => Promise<void> | void;
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
119
|
+
onBeforeStart?: (instance: BXO) => Promise<void> | void;
|
120
|
+
onAfterStart?: (instance: BXO) => Promise<void> | void;
|
121
|
+
onBeforeStop?: (instance: BXO) => Promise<void> | void;
|
122
|
+
onAfterStop?: (instance: BXO) => Promise<void> | void;
|
123
|
+
onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
|
124
|
+
onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
|
125
|
+
onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
|
126
|
+
}
|
127
|
+
|
128
|
+
// Add global options for BXO
|
129
|
+
interface BXOOptions {
|
130
|
+
enableValidation?: boolean;
|
53
131
|
}
|
54
132
|
|
55
133
|
export default class BXO {
|
56
134
|
private _routes: Route[] = [];
|
135
|
+
private _wsRoutes: WSRoute[] = [];
|
57
136
|
private plugins: BXO[] = [];
|
58
137
|
private hooks: LifecycleHooks = {};
|
59
138
|
private server?: any;
|
60
139
|
private isRunning: boolean = false;
|
61
|
-
private hotReloadEnabled: boolean = false;
|
62
|
-
private watchedFiles: Set<string> = new Set();
|
63
|
-
private watchedExclude: Set<string> = new Set();
|
64
140
|
private serverPort?: number;
|
65
141
|
private serverHostname?: string;
|
142
|
+
private enableValidation: boolean = true;
|
66
143
|
|
67
|
-
constructor() {
|
144
|
+
constructor(options?: BXOOptions) {
|
145
|
+
this.enableValidation = options?.enableValidation ?? true;
|
146
|
+
}
|
68
147
|
|
69
148
|
// Lifecycle hook methods
|
70
|
-
onBeforeStart(handler: () => Promise<void> | void): this {
|
149
|
+
onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
|
71
150
|
this.hooks.onBeforeStart = handler;
|
72
151
|
return this;
|
73
152
|
}
|
74
153
|
|
75
|
-
onAfterStart(handler: () => Promise<void> | void): this {
|
154
|
+
onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
|
76
155
|
this.hooks.onAfterStart = handler;
|
77
156
|
return this;
|
78
157
|
}
|
79
158
|
|
80
|
-
onBeforeStop(handler: () => Promise<void> | void): this {
|
159
|
+
onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
|
81
160
|
this.hooks.onBeforeStop = handler;
|
82
161
|
return this;
|
83
162
|
}
|
84
163
|
|
85
|
-
onAfterStop(handler: () => Promise<void> | void): this {
|
164
|
+
onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
|
86
165
|
this.hooks.onAfterStop = handler;
|
87
166
|
return this;
|
88
167
|
}
|
89
168
|
|
90
|
-
onBeforeRestart(handler: () => Promise<void> | void): this {
|
91
|
-
this.hooks.onBeforeRestart = handler;
|
92
|
-
return this;
|
93
|
-
}
|
94
169
|
|
95
|
-
onAfterRestart(handler: () => Promise<void> | void): this {
|
96
|
-
this.hooks.onAfterRestart = handler;
|
97
|
-
return this;
|
98
|
-
}
|
99
170
|
|
100
|
-
onRequest(handler: (ctx: Context) => Promise<void> | void): this {
|
171
|
+
onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
|
101
172
|
this.hooks.onRequest = handler;
|
102
173
|
return this;
|
103
174
|
}
|
104
175
|
|
105
|
-
onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
|
176
|
+
onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
|
106
177
|
this.hooks.onResponse = handler;
|
107
178
|
return this;
|
108
179
|
}
|
109
180
|
|
110
|
-
onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
|
181
|
+
onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
|
111
182
|
this.hooks.onError = handler;
|
112
183
|
return this;
|
113
184
|
}
|
@@ -209,24 +280,135 @@ export default class BXO {
|
|
209
280
|
return this;
|
210
281
|
}
|
211
282
|
|
283
|
+
// WebSocket route handler
|
284
|
+
ws(path: string, handler: WebSocketHandler): this {
|
285
|
+
this._wsRoutes.push({ path, handler });
|
286
|
+
return this;
|
287
|
+
}
|
288
|
+
|
289
|
+
// Helper methods to get all routes including plugin routes
|
290
|
+
private getAllRoutes(): Route[] {
|
291
|
+
const allRoutes = [...this._routes];
|
292
|
+
for (const plugin of this.plugins) {
|
293
|
+
allRoutes.push(...plugin._routes);
|
294
|
+
}
|
295
|
+
return allRoutes;
|
296
|
+
}
|
297
|
+
|
298
|
+
private getAllWSRoutes(): WSRoute[] {
|
299
|
+
const allWSRoutes = [...this._wsRoutes];
|
300
|
+
for (const plugin of this.plugins) {
|
301
|
+
allWSRoutes.push(...plugin._wsRoutes);
|
302
|
+
}
|
303
|
+
return allWSRoutes;
|
304
|
+
}
|
305
|
+
|
212
306
|
// Route matching utility
|
213
307
|
private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
|
214
|
-
|
308
|
+
const allRoutes = this.getAllRoutes();
|
309
|
+
|
310
|
+
for (const route of allRoutes) {
|
215
311
|
if (route.method !== method) continue;
|
216
312
|
|
217
313
|
const routeSegments = route.path.split('/').filter(Boolean);
|
218
314
|
const pathSegments = pathname.split('/').filter(Boolean);
|
219
315
|
|
220
|
-
|
316
|
+
const params: Record<string, string> = {};
|
317
|
+
let isMatch = true;
|
318
|
+
|
319
|
+
// Handle wildcard at the end (catch-all)
|
320
|
+
const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
|
321
|
+
|
322
|
+
if (hasWildcardAtEnd) {
|
323
|
+
// For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
|
324
|
+
if (pathSegments.length < routeSegments.length - 1) continue;
|
325
|
+
} else {
|
326
|
+
// For exact matching (with possible single-segment wildcards), lengths must match
|
327
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
328
|
+
}
|
329
|
+
|
330
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
331
|
+
const routeSegment = routeSegments[i];
|
332
|
+
const pathSegment = pathSegments[i];
|
333
|
+
|
334
|
+
if (!routeSegment) {
|
335
|
+
isMatch = false;
|
336
|
+
break;
|
337
|
+
}
|
338
|
+
|
339
|
+
// Handle catch-all wildcard at the end
|
340
|
+
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
341
|
+
// Wildcard at end matches remaining path segments
|
342
|
+
const remainingPath = pathSegments.slice(i).join('/');
|
343
|
+
params['*'] = decodeURIComponent(remainingPath);
|
344
|
+
break;
|
345
|
+
}
|
346
|
+
|
347
|
+
if (!pathSegment) {
|
348
|
+
isMatch = false;
|
349
|
+
break;
|
350
|
+
}
|
351
|
+
|
352
|
+
if (routeSegment.startsWith(':')) {
|
353
|
+
const paramName = routeSegment.slice(1);
|
354
|
+
params[paramName] = decodeURIComponent(pathSegment);
|
355
|
+
} else if (routeSegment === '*') {
|
356
|
+
// Single segment wildcard
|
357
|
+
params['*'] = decodeURIComponent(pathSegment);
|
358
|
+
} else if (routeSegment !== decodeURIComponent(pathSegment)) {
|
359
|
+
isMatch = false;
|
360
|
+
break;
|
361
|
+
}
|
362
|
+
}
|
363
|
+
|
364
|
+
if (isMatch) {
|
365
|
+
return { route, params };
|
366
|
+
}
|
367
|
+
}
|
368
|
+
|
369
|
+
return null;
|
370
|
+
}
|
371
|
+
|
372
|
+
// WebSocket route matching utility
|
373
|
+
private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
|
374
|
+
const allWSRoutes = this.getAllWSRoutes();
|
375
|
+
|
376
|
+
for (const route of allWSRoutes) {
|
377
|
+
const routeSegments = route.path.split('/').filter(Boolean);
|
378
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
221
379
|
|
222
380
|
const params: Record<string, string> = {};
|
223
381
|
let isMatch = true;
|
224
382
|
|
383
|
+
// Handle wildcard at the end (catch-all)
|
384
|
+
const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
|
385
|
+
|
386
|
+
if (hasWildcardAtEnd) {
|
387
|
+
// For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
|
388
|
+
if (pathSegments.length < routeSegments.length - 1) continue;
|
389
|
+
} else {
|
390
|
+
// For exact matching (with possible single-segment wildcards), lengths must match
|
391
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
392
|
+
}
|
393
|
+
|
225
394
|
for (let i = 0; i < routeSegments.length; i++) {
|
226
395
|
const routeSegment = routeSegments[i];
|
227
396
|
const pathSegment = pathSegments[i];
|
228
397
|
|
229
|
-
if (!routeSegment
|
398
|
+
if (!routeSegment) {
|
399
|
+
isMatch = false;
|
400
|
+
break;
|
401
|
+
}
|
402
|
+
|
403
|
+
// Handle catch-all wildcard at the end
|
404
|
+
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
405
|
+
// Wildcard at end matches remaining path segments
|
406
|
+
const remainingPath = pathSegments.slice(i).join('/');
|
407
|
+
params['*'] = decodeURIComponent(remainingPath);
|
408
|
+
break;
|
409
|
+
}
|
410
|
+
|
411
|
+
if (!pathSegment) {
|
230
412
|
isMatch = false;
|
231
413
|
break;
|
232
414
|
}
|
@@ -234,7 +416,10 @@ export default class BXO {
|
|
234
416
|
if (routeSegment.startsWith(':')) {
|
235
417
|
const paramName = routeSegment.slice(1);
|
236
418
|
params[paramName] = decodeURIComponent(pathSegment);
|
237
|
-
} else if (routeSegment
|
419
|
+
} else if (routeSegment === '*') {
|
420
|
+
// Single segment wildcard
|
421
|
+
params['*'] = decodeURIComponent(pathSegment);
|
422
|
+
} else if (routeSegment !== decodeURIComponent(pathSegment)) {
|
238
423
|
isMatch = false;
|
239
424
|
break;
|
240
425
|
}
|
@@ -251,33 +436,101 @@ export default class BXO {
|
|
251
436
|
// Parse query string
|
252
437
|
private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
|
253
438
|
const query: Record<string, string | undefined> = {};
|
254
|
-
|
439
|
+
searchParams.forEach((value, key) => {
|
255
440
|
query[key] = value;
|
256
|
-
}
|
441
|
+
});
|
257
442
|
return query;
|
258
443
|
}
|
259
444
|
|
260
445
|
// Parse headers
|
261
446
|
private parseHeaders(headers: Headers): Record<string, string> {
|
262
447
|
const headerObj: Record<string, string> = {};
|
263
|
-
|
448
|
+
headers.forEach((value, key) => {
|
264
449
|
headerObj[key] = value;
|
265
|
-
}
|
450
|
+
});
|
266
451
|
return headerObj;
|
267
452
|
}
|
268
453
|
|
454
|
+
// Parse cookies from Cookie header
|
455
|
+
private parseCookies(cookieHeader: string | null): Record<string, string> {
|
456
|
+
const cookies: Record<string, string> = {};
|
457
|
+
|
458
|
+
if (!cookieHeader) return cookies;
|
459
|
+
|
460
|
+
const cookiePairs = cookieHeader.split(';');
|
461
|
+
for (const pair of cookiePairs) {
|
462
|
+
const [name, value] = pair.trim().split('=');
|
463
|
+
if (name && value) {
|
464
|
+
cookies[decodeURIComponent(name)] = decodeURIComponent(value);
|
465
|
+
}
|
466
|
+
}
|
467
|
+
|
468
|
+
return cookies;
|
469
|
+
}
|
470
|
+
|
269
471
|
// Validate data against Zod schema
|
270
472
|
private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
|
271
473
|
if (!schema) return data;
|
272
474
|
return schema.parse(data);
|
273
475
|
}
|
274
476
|
|
477
|
+
// Validate response against response config (supports both simple and status-based schemas)
|
478
|
+
private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
|
479
|
+
if (!responseConfig || !this.enableValidation) return data;
|
480
|
+
|
481
|
+
// If it's a simple schema (not status-based)
|
482
|
+
if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
|
483
|
+
return responseConfig.parse(data);
|
484
|
+
}
|
485
|
+
|
486
|
+
// If it's a status-based schema
|
487
|
+
if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
|
488
|
+
const statusSchema = responseConfig[status];
|
489
|
+
if (statusSchema) {
|
490
|
+
return statusSchema.parse(data);
|
491
|
+
}
|
492
|
+
|
493
|
+
// If no specific status schema found, try to find a fallback
|
494
|
+
// Common fallback statuses: 200, 201, 400, 500
|
495
|
+
const fallbackStatuses = [200, 201, 400, 500];
|
496
|
+
for (const fallbackStatus of fallbackStatuses) {
|
497
|
+
if (responseConfig[fallbackStatus]) {
|
498
|
+
return responseConfig[fallbackStatus].parse(data);
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
// If no schema found for the status, return data as-is
|
503
|
+
return data;
|
504
|
+
}
|
505
|
+
|
506
|
+
return data;
|
507
|
+
}
|
508
|
+
|
275
509
|
// Main request handler
|
276
|
-
private async handleRequest(request: Request): Promise<Response> {
|
510
|
+
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
277
511
|
const url = new URL(request.url);
|
278
512
|
const method = request.method;
|
279
513
|
const pathname = url.pathname;
|
280
514
|
|
515
|
+
// Check for WebSocket upgrade
|
516
|
+
if (request.headers.get('upgrade') === 'websocket') {
|
517
|
+
const wsMatchResult = this.matchWSRoute(pathname);
|
518
|
+
if (wsMatchResult && server) {
|
519
|
+
const success = server.upgrade(request, {
|
520
|
+
data: {
|
521
|
+
handler: wsMatchResult.route.handler,
|
522
|
+
params: wsMatchResult.params,
|
523
|
+
pathname
|
524
|
+
}
|
525
|
+
});
|
526
|
+
|
527
|
+
if (success) {
|
528
|
+
return; // undefined response means upgrade was successful
|
529
|
+
}
|
530
|
+
}
|
531
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
532
|
+
}
|
533
|
+
|
281
534
|
const matchResult = this.matchRoute(method, pathname);
|
282
535
|
if (!matchResult) {
|
283
536
|
return new Response('Not Found', { status: 404 });
|
@@ -286,6 +539,7 @@ export default class BXO {
|
|
286
539
|
const { route, params } = matchResult;
|
287
540
|
const query = this.parseQuery(url.searchParams);
|
288
541
|
const headers = this.parseHeaders(request.headers);
|
542
|
+
const cookies = this.parseCookies(request.headers.get('cookie'));
|
289
543
|
|
290
544
|
let body: any;
|
291
545
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
@@ -300,30 +554,82 @@ export default class BXO {
|
|
300
554
|
const formData = await request.formData();
|
301
555
|
body = Object.fromEntries(formData.entries());
|
302
556
|
} else {
|
303
|
-
|
557
|
+
// Try to parse as JSON if it looks like JSON, otherwise treat as text
|
558
|
+
const textBody = await request.text();
|
559
|
+
try {
|
560
|
+
// Check if the text looks like JSON
|
561
|
+
if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
|
562
|
+
body = JSON.parse(textBody);
|
563
|
+
} else {
|
564
|
+
body = textBody;
|
565
|
+
}
|
566
|
+
} catch {
|
567
|
+
body = textBody;
|
568
|
+
}
|
304
569
|
}
|
305
570
|
}
|
306
571
|
|
307
|
-
// Create context
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
572
|
+
// Create context with validation
|
573
|
+
let ctx: Context;
|
574
|
+
try {
|
575
|
+
// Validate each part separately to get better error messages
|
576
|
+
const validatedParams = this.enableValidation && route.config?.params ? this.validateData(route.config.params, params) : params;
|
577
|
+
const validatedQuery = this.enableValidation && route.config?.query ? this.validateData(route.config.query, query) : query;
|
578
|
+
const validatedBody = this.enableValidation && route.config?.body ? this.validateData(route.config.body, body) : body;
|
579
|
+
const validatedHeaders = this.enableValidation && route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
|
580
|
+
const validatedCookies = this.enableValidation && route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
|
581
|
+
|
582
|
+
ctx = {
|
583
|
+
params: validatedParams,
|
584
|
+
query: validatedQuery,
|
585
|
+
body: validatedBody,
|
586
|
+
headers: validatedHeaders,
|
587
|
+
cookies: validatedCookies,
|
588
|
+
path: pathname,
|
589
|
+
request,
|
590
|
+
set: {},
|
591
|
+
status: ((code: number, data?: any) => {
|
592
|
+
ctx.set.status = code;
|
593
|
+
return data;
|
594
|
+
}) as any
|
595
|
+
};
|
596
|
+
} catch (validationError) {
|
597
|
+
// Validation failed - return error response
|
598
|
+
|
599
|
+
// Extract detailed validation errors from Zod
|
600
|
+
let validationDetails = undefined;
|
601
|
+
if (validationError instanceof Error) {
|
602
|
+
if ('errors' in validationError && Array.isArray(validationError.errors)) {
|
603
|
+
validationDetails = validationError.errors;
|
604
|
+
} else if ('issues' in validationError && Array.isArray(validationError.issues)) {
|
605
|
+
validationDetails = validationError.issues;
|
606
|
+
}
|
607
|
+
}
|
608
|
+
|
609
|
+
// Create a clean error message
|
610
|
+
const errorMessage = validationDetails && validationDetails.length > 0
|
611
|
+
? `Validation failed for ${validationDetails.length} field(s)`
|
612
|
+
: 'Validation failed';
|
613
|
+
|
614
|
+
return new Response(JSON.stringify({
|
615
|
+
error: errorMessage,
|
616
|
+
details: validationDetails
|
617
|
+
}), {
|
618
|
+
status: 400,
|
619
|
+
headers: { 'Content-Type': 'application/json' }
|
620
|
+
});
|
621
|
+
}
|
316
622
|
|
317
623
|
try {
|
318
624
|
// Run global onRequest hook
|
319
625
|
if (this.hooks.onRequest) {
|
320
|
-
await this.hooks.onRequest(ctx);
|
626
|
+
await this.hooks.onRequest(ctx, this);
|
321
627
|
}
|
322
628
|
|
323
629
|
// Run BXO instance onRequest hooks
|
324
630
|
for (const bxoInstance of this.plugins) {
|
325
631
|
if (bxoInstance.hooks.onRequest) {
|
326
|
-
await bxoInstance.hooks.onRequest(ctx);
|
632
|
+
await bxoInstance.hooks.onRequest(ctx, this);
|
327
633
|
}
|
328
634
|
}
|
329
635
|
|
@@ -332,24 +638,43 @@ export default class BXO {
|
|
332
638
|
|
333
639
|
// Run global onResponse hook
|
334
640
|
if (this.hooks.onResponse) {
|
335
|
-
response = await this.hooks.onResponse(ctx, response) || response;
|
641
|
+
response = await this.hooks.onResponse(ctx, response, this) || response;
|
336
642
|
}
|
337
643
|
|
338
644
|
// Run BXO instance onResponse hooks
|
339
645
|
for (const bxoInstance of this.plugins) {
|
340
646
|
if (bxoInstance.hooks.onResponse) {
|
341
|
-
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
647
|
+
response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
|
342
648
|
}
|
343
649
|
}
|
344
650
|
|
345
651
|
// Validate response against schema if provided
|
346
|
-
if (route.config?.response && !(response instanceof Response)) {
|
652
|
+
if (this.enableValidation && route.config?.response && !(response instanceof Response)) {
|
347
653
|
try {
|
348
|
-
|
654
|
+
const status = ctx.set.status || 200;
|
655
|
+
response = this.validateResponse(route.config.response, response, status);
|
349
656
|
} catch (validationError) {
|
350
657
|
// Response validation failed
|
351
|
-
|
352
|
-
|
658
|
+
|
659
|
+
// Extract detailed validation errors from Zod
|
660
|
+
let validationDetails = undefined;
|
661
|
+
if (validationError instanceof Error) {
|
662
|
+
if ('errors' in validationError && Array.isArray(validationError.errors)) {
|
663
|
+
validationDetails = validationError.errors;
|
664
|
+
} else if ('issues' in validationError && Array.isArray(validationError.issues)) {
|
665
|
+
validationDetails = validationError.issues;
|
666
|
+
}
|
667
|
+
}
|
668
|
+
|
669
|
+
// Create a clean error message
|
670
|
+
const errorMessage = validationDetails && validationDetails.length > 0
|
671
|
+
? `Response validation failed for ${validationDetails.length} field(s)`
|
672
|
+
: 'Response validation failed';
|
673
|
+
|
674
|
+
return new Response(JSON.stringify({
|
675
|
+
error: errorMessage,
|
676
|
+
details: validationDetails
|
677
|
+
}), {
|
353
678
|
status: 500,
|
354
679
|
headers: { 'Content-Type': 'application/json' }
|
355
680
|
});
|
@@ -361,9 +686,63 @@ export default class BXO {
|
|
361
686
|
return response;
|
362
687
|
}
|
363
688
|
|
689
|
+
// Handle File response (like Elysia)
|
690
|
+
if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
|
691
|
+
const file = response as File;
|
692
|
+
const responseInit: ResponseInit = {
|
693
|
+
status: ctx.set.status || 200,
|
694
|
+
headers: {
|
695
|
+
'Content-Type': file.type || 'application/octet-stream',
|
696
|
+
'Content-Length': file.size.toString(),
|
697
|
+
...ctx.set.headers
|
698
|
+
}
|
699
|
+
};
|
700
|
+
return new Response(file, responseInit);
|
701
|
+
}
|
702
|
+
|
703
|
+
// Handle Bun.file() response
|
704
|
+
if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
|
705
|
+
const bunFile = response as any;
|
706
|
+
const responseInit: ResponseInit = {
|
707
|
+
status: ctx.set.status || 200,
|
708
|
+
headers: {
|
709
|
+
'Content-Type': bunFile.type || 'application/octet-stream',
|
710
|
+
'Content-Length': bunFile.size?.toString() || '',
|
711
|
+
...ctx.set.headers,
|
712
|
+
...(bunFile.headers || {}) // Support custom headers from file helper
|
713
|
+
}
|
714
|
+
};
|
715
|
+
return new Response(bunFile, responseInit);
|
716
|
+
}
|
717
|
+
|
718
|
+
// Prepare headers with cookies
|
719
|
+
let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
|
720
|
+
|
721
|
+
// Handle cookies if any are set
|
722
|
+
if (ctx.set.cookies && ctx.set.cookies.length > 0) {
|
723
|
+
const cookieHeaders = ctx.set.cookies.map(cookie => {
|
724
|
+
let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
725
|
+
|
726
|
+
if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
|
727
|
+
if (cookie.path) cookieString += `; Path=${cookie.path}`;
|
728
|
+
if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
|
729
|
+
if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
|
730
|
+
if (cookie.secure) cookieString += `; Secure`;
|
731
|
+
if (cookie.httpOnly) cookieString += `; HttpOnly`;
|
732
|
+
if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
|
733
|
+
|
734
|
+
return cookieString;
|
735
|
+
});
|
736
|
+
|
737
|
+
// Add Set-Cookie headers
|
738
|
+
cookieHeaders.forEach((cookieHeader, index) => {
|
739
|
+
responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
|
740
|
+
});
|
741
|
+
}
|
742
|
+
|
364
743
|
const responseInit: ResponseInit = {
|
365
744
|
status: ctx.set.status || 200,
|
366
|
-
headers:
|
745
|
+
headers: responseHeaders
|
367
746
|
};
|
368
747
|
|
369
748
|
if (typeof response === 'string') {
|
@@ -383,12 +762,12 @@ export default class BXO {
|
|
383
762
|
let errorResponse: any;
|
384
763
|
|
385
764
|
if (this.hooks.onError) {
|
386
|
-
errorResponse = await this.hooks.onError(ctx, error as Error);
|
765
|
+
errorResponse = await this.hooks.onError(ctx, error as Error, this);
|
387
766
|
}
|
388
767
|
|
389
768
|
for (const bxoInstance of this.plugins) {
|
390
769
|
if (bxoInstance.hooks.onError) {
|
391
|
-
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
770
|
+
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
|
392
771
|
}
|
393
772
|
}
|
394
773
|
|
@@ -411,77 +790,7 @@ export default class BXO {
|
|
411
790
|
}
|
412
791
|
}
|
413
792
|
|
414
|
-
// Hot reload functionality
|
415
|
-
enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
|
416
|
-
this.hotReloadEnabled = true;
|
417
|
-
watchPaths.forEach(path => this.watchedFiles.add(path));
|
418
|
-
excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
|
419
|
-
return this;
|
420
|
-
}
|
421
793
|
|
422
|
-
private shouldExcludeFile(filename: string): boolean {
|
423
|
-
for (const pattern of this.watchedExclude) {
|
424
|
-
// Handle exact match
|
425
|
-
if (pattern === filename) {
|
426
|
-
return true;
|
427
|
-
}
|
428
|
-
|
429
|
-
// Handle directory patterns (e.g., "node_modules/", "dist/")
|
430
|
-
if (pattern.endsWith('/')) {
|
431
|
-
if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
|
432
|
-
return true;
|
433
|
-
}
|
434
|
-
}
|
435
|
-
|
436
|
-
// Handle wildcard patterns (e.g., "*.log", "temp*")
|
437
|
-
if (pattern.includes('*')) {
|
438
|
-
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
439
|
-
if (regex.test(filename)) {
|
440
|
-
return true;
|
441
|
-
}
|
442
|
-
}
|
443
|
-
|
444
|
-
// Handle file extension patterns (e.g., ".log", ".tmp")
|
445
|
-
if (pattern.startsWith('.') && filename.endsWith(pattern)) {
|
446
|
-
return true;
|
447
|
-
}
|
448
|
-
|
449
|
-
// Handle substring matches for directories
|
450
|
-
if (filename.includes(pattern)) {
|
451
|
-
return true;
|
452
|
-
}
|
453
|
-
}
|
454
|
-
|
455
|
-
return false;
|
456
|
-
}
|
457
|
-
|
458
|
-
private async setupFileWatcher(port: number, hostname: string): Promise<void> {
|
459
|
-
if (!this.hotReloadEnabled) return;
|
460
|
-
|
461
|
-
const fs = require('fs');
|
462
|
-
|
463
|
-
for (const watchPath of this.watchedFiles) {
|
464
|
-
try {
|
465
|
-
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
466
|
-
if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
|
467
|
-
// Check if file should be excluded
|
468
|
-
if (this.shouldExcludeFile(filename)) {
|
469
|
-
return;
|
470
|
-
}
|
471
|
-
|
472
|
-
console.log(`🔄 File changed: ${filename}, restarting server...`);
|
473
|
-
await this.restart(port, hostname);
|
474
|
-
}
|
475
|
-
});
|
476
|
-
console.log(`👀 Watching ${watchPath} for changes...`);
|
477
|
-
if (this.watchedExclude.size > 0) {
|
478
|
-
console.log(`🚫 Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
|
479
|
-
}
|
480
|
-
} catch (error) {
|
481
|
-
console.warn(`⚠️ Could not watch ${watchPath}:`, error);
|
482
|
-
}
|
483
|
-
}
|
484
|
-
}
|
485
794
|
|
486
795
|
// Server management methods
|
487
796
|
async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
@@ -493,29 +802,49 @@ export default class BXO {
|
|
493
802
|
try {
|
494
803
|
// Before start hook
|
495
804
|
if (this.hooks.onBeforeStart) {
|
496
|
-
await this.hooks.onBeforeStart();
|
805
|
+
await this.hooks.onBeforeStart(this);
|
497
806
|
}
|
498
807
|
|
499
808
|
this.server = Bun.serve({
|
500
809
|
port,
|
501
810
|
hostname,
|
502
|
-
fetch: (request) => this.handleRequest(request),
|
811
|
+
fetch: (request, server) => this.handleRequest(request, server),
|
812
|
+
websocket: {
|
813
|
+
message: (ws: any, message: any) => {
|
814
|
+
const handler = ws.data?.handler;
|
815
|
+
if (handler?.onMessage) {
|
816
|
+
handler.onMessage(ws, message);
|
817
|
+
}
|
818
|
+
},
|
819
|
+
open: (ws: any) => {
|
820
|
+
const handler = ws.data?.handler;
|
821
|
+
if (handler?.onOpen) {
|
822
|
+
handler.onOpen(ws);
|
823
|
+
}
|
824
|
+
},
|
825
|
+
close: (ws: any, code?: number, reason?: string) => {
|
826
|
+
const handler = ws.data?.handler;
|
827
|
+
if (handler?.onClose) {
|
828
|
+
handler.onClose(ws, code, reason);
|
829
|
+
}
|
830
|
+
}
|
831
|
+
}
|
503
832
|
});
|
504
833
|
|
834
|
+
// Verify server was created successfully
|
835
|
+
if (!this.server) {
|
836
|
+
throw new Error('Failed to create server instance');
|
837
|
+
}
|
838
|
+
|
505
839
|
this.isRunning = true;
|
506
840
|
this.serverPort = port;
|
507
841
|
this.serverHostname = hostname;
|
508
842
|
|
509
|
-
console.log(`🦊 BXO server running at http://${hostname}:${port}`);
|
510
|
-
|
511
843
|
// After start hook
|
512
844
|
if (this.hooks.onAfterStart) {
|
513
|
-
await this.hooks.onAfterStart();
|
845
|
+
await this.hooks.onAfterStart(this);
|
514
846
|
}
|
515
847
|
|
516
|
-
// Setup hot reload
|
517
|
-
await this.setupFileWatcher(port, hostname);
|
518
|
-
|
519
848
|
// Handle graceful shutdown
|
520
849
|
const shutdownHandler = async () => {
|
521
850
|
await this.stop();
|
@@ -540,57 +869,49 @@ export default class BXO {
|
|
540
869
|
try {
|
541
870
|
// Before stop hook
|
542
871
|
if (this.hooks.onBeforeStop) {
|
543
|
-
await this.hooks.onBeforeStop();
|
872
|
+
await this.hooks.onBeforeStop(this);
|
544
873
|
}
|
545
874
|
|
546
875
|
if (this.server) {
|
547
|
-
|
548
|
-
|
876
|
+
try {
|
877
|
+
// Try to stop the server gracefully
|
878
|
+
if (typeof this.server.stop === 'function') {
|
879
|
+
this.server.stop();
|
880
|
+
} else {
|
881
|
+
console.warn('⚠️ Server stop method not available');
|
882
|
+
}
|
883
|
+
} catch (stopError) {
|
884
|
+
console.error('❌ Error calling server.stop():', stopError);
|
885
|
+
}
|
886
|
+
|
887
|
+
// Clear the server reference
|
888
|
+
this.server = undefined;
|
549
889
|
}
|
550
890
|
|
891
|
+
// Reset state regardless of server.stop() success
|
551
892
|
this.isRunning = false;
|
552
893
|
this.serverPort = undefined;
|
553
894
|
this.serverHostname = undefined;
|
554
895
|
|
555
|
-
console.log('🛑 BXO server stopped');
|
556
|
-
|
557
896
|
// After stop hook
|
558
897
|
if (this.hooks.onAfterStop) {
|
559
|
-
await this.hooks.onAfterStop();
|
898
|
+
await this.hooks.onAfterStop(this);
|
560
899
|
}
|
561
900
|
|
901
|
+
console.log('✅ Server stopped successfully');
|
902
|
+
|
562
903
|
} catch (error) {
|
563
904
|
console.error('❌ Error stopping server:', error);
|
905
|
+
// Even if there's an error, reset the state
|
906
|
+
this.isRunning = false;
|
907
|
+
this.server = undefined;
|
908
|
+
this.serverPort = undefined;
|
909
|
+
this.serverHostname = undefined;
|
564
910
|
throw error;
|
565
911
|
}
|
566
912
|
}
|
567
913
|
|
568
|
-
async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
569
|
-
try {
|
570
|
-
// Before restart hook
|
571
|
-
if (this.hooks.onBeforeRestart) {
|
572
|
-
await this.hooks.onBeforeRestart();
|
573
|
-
}
|
574
914
|
|
575
|
-
console.log('🔄 Restarting BXO server...');
|
576
|
-
|
577
|
-
await this.stop();
|
578
|
-
|
579
|
-
// Small delay to ensure cleanup
|
580
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
581
|
-
|
582
|
-
await this.start(port, hostname);
|
583
|
-
|
584
|
-
// After restart hook
|
585
|
-
if (this.hooks.onAfterRestart) {
|
586
|
-
await this.hooks.onAfterRestart();
|
587
|
-
}
|
588
|
-
|
589
|
-
} catch (error) {
|
590
|
-
console.error('❌ Error restarting server:', error);
|
591
|
-
throw error;
|
592
|
-
}
|
593
|
-
}
|
594
915
|
|
595
916
|
// Backward compatibility
|
596
917
|
async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
@@ -599,41 +920,38 @@ export default class BXO {
|
|
599
920
|
|
600
921
|
// Server status
|
601
922
|
isServerRunning(): boolean {
|
602
|
-
return this.isRunning;
|
923
|
+
return this.isRunning && this.server !== undefined;
|
603
924
|
}
|
604
925
|
|
605
|
-
getServerInfo(): { running: boolean
|
926
|
+
getServerInfo(): { running: boolean } {
|
606
927
|
return {
|
607
|
-
running: this.isRunning
|
608
|
-
hotReload: this.hotReloadEnabled,
|
609
|
-
watchedFiles: Array.from(this.watchedFiles),
|
610
|
-
excludePatterns: Array.from(this.watchedExclude)
|
928
|
+
running: this.isRunning
|
611
929
|
};
|
612
930
|
}
|
613
931
|
|
614
932
|
// Get server information (alias for getServerInfo)
|
615
933
|
get info() {
|
934
|
+
// Calculate total routes including plugins
|
935
|
+
const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
|
936
|
+
const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
|
937
|
+
|
616
938
|
return {
|
617
939
|
// Server status
|
618
940
|
running: this.isRunning,
|
619
941
|
server: this.server ? 'Bun' : null,
|
620
|
-
|
942
|
+
|
621
943
|
// Connection details
|
622
944
|
hostname: this.serverHostname,
|
623
945
|
port: this.serverPort,
|
624
|
-
url: this.isRunning && this.serverHostname && this.serverPort
|
625
|
-
? `http://${this.serverHostname}:${this.serverPort}`
|
946
|
+
url: this.isRunning && this.serverHostname && this.serverPort
|
947
|
+
? `http://${this.serverHostname}:${this.serverPort}`
|
626
948
|
: null,
|
627
|
-
|
949
|
+
|
628
950
|
// Application statistics
|
629
|
-
totalRoutes
|
951
|
+
totalRoutes,
|
952
|
+
totalWsRoutes,
|
630
953
|
totalPlugins: this.plugins.length,
|
631
|
-
|
632
|
-
// Hot reload configuration
|
633
|
-
hotReload: this.hotReloadEnabled,
|
634
|
-
watchedFiles: Array.from(this.watchedFiles),
|
635
|
-
excludePatterns: Array.from(this.watchedExclude),
|
636
|
-
|
954
|
+
|
637
955
|
// System information
|
638
956
|
runtime: 'Bun',
|
639
957
|
version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
|
@@ -644,27 +962,96 @@ export default class BXO {
|
|
644
962
|
|
645
963
|
// Get all routes information
|
646
964
|
get routes() {
|
647
|
-
|
965
|
+
// Get routes from main instance
|
966
|
+
const mainRoutes = this._routes.map((route: Route) => ({
|
648
967
|
method: route.method,
|
649
968
|
path: route.path,
|
650
969
|
hasConfig: !!route.config,
|
651
|
-
config: route.config
|
652
|
-
|
653
|
-
hasQuery: !!route.config.query,
|
654
|
-
hasBody: !!route.config.body,
|
655
|
-
hasHeaders: !!route.config.headers,
|
656
|
-
hasResponse: !!route.config.response
|
657
|
-
} : null
|
970
|
+
config: route.config || null,
|
971
|
+
source: 'main' as const
|
658
972
|
}));
|
973
|
+
|
974
|
+
// Get routes from all plugins
|
975
|
+
const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
|
976
|
+
plugin._routes.map((route: Route) => ({
|
977
|
+
method: route.method,
|
978
|
+
path: route.path,
|
979
|
+
hasConfig: !!route.config,
|
980
|
+
config: route.config || null,
|
981
|
+
source: 'plugin' as const,
|
982
|
+
pluginIndex
|
983
|
+
}))
|
984
|
+
);
|
985
|
+
|
986
|
+
return [...mainRoutes, ...pluginRoutes];
|
987
|
+
}
|
988
|
+
|
989
|
+
// Get all WebSocket routes information
|
990
|
+
get wsRoutes() {
|
991
|
+
// Get WebSocket routes from main instance
|
992
|
+
const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
|
993
|
+
path: route.path,
|
994
|
+
hasHandlers: {
|
995
|
+
onOpen: !!route.handler.onOpen,
|
996
|
+
onMessage: !!route.handler.onMessage,
|
997
|
+
onClose: !!route.handler.onClose,
|
998
|
+
onError: !!route.handler.onError
|
999
|
+
},
|
1000
|
+
source: 'main' as const
|
1001
|
+
}));
|
1002
|
+
|
1003
|
+
// Get WebSocket routes from all plugins
|
1004
|
+
const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
|
1005
|
+
plugin._wsRoutes.map((route: WSRoute) => ({
|
1006
|
+
path: route.path,
|
1007
|
+
hasHandlers: {
|
1008
|
+
onOpen: !!route.handler.onOpen,
|
1009
|
+
onMessage: !!route.handler.onMessage,
|
1010
|
+
onClose: !!route.handler.onClose,
|
1011
|
+
onError: !!route.handler.onError
|
1012
|
+
},
|
1013
|
+
source: 'plugin' as const,
|
1014
|
+
pluginIndex
|
1015
|
+
}))
|
1016
|
+
);
|
1017
|
+
|
1018
|
+
return [...mainWsRoutes, ...pluginWsRoutes];
|
659
1019
|
}
|
660
1020
|
}
|
661
1021
|
|
662
|
-
const error = (error: Error, status: number = 500) => {
|
663
|
-
return new Response(JSON.stringify({ error: error.message }), { status });
|
1022
|
+
const error = (error: Error | string, status: number = 500) => {
|
1023
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
|
1024
|
+
}
|
1025
|
+
|
1026
|
+
// File helper function (like Elysia)
|
1027
|
+
const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
|
1028
|
+
const bunFile = Bun.file(path);
|
1029
|
+
|
1030
|
+
if (options?.type) {
|
1031
|
+
// Create a wrapper to override the MIME type
|
1032
|
+
return {
|
1033
|
+
...bunFile,
|
1034
|
+
type: options.type,
|
1035
|
+
headers: options.headers
|
1036
|
+
};
|
1037
|
+
}
|
1038
|
+
|
1039
|
+
return bunFile;
|
664
1040
|
}
|
665
1041
|
|
666
1042
|
// Export Zod for convenience
|
667
|
-
export { z, error };
|
1043
|
+
export { z, error, file };
|
668
1044
|
|
669
1045
|
// Export types for external use
|
670
|
-
export type { RouteConfig, Handler };
|
1046
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions };
|
1047
|
+
|
1048
|
+
// Helper function to create a cookie
|
1049
|
+
export const createCookie = (
|
1050
|
+
name: string,
|
1051
|
+
value: string,
|
1052
|
+
options: Omit<Cookie, 'name' | 'value'> = {}
|
1053
|
+
): Cookie => ({
|
1054
|
+
name,
|
1055
|
+
value,
|
1056
|
+
...options
|
1057
|
+
});
|