bxo 0.0.5-dev.51 → 0.0.5-dev.53

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/index.ts CHANGED
@@ -1,1550 +1,5 @@
1
- import { z } from 'zod';
1
+ // Re-export everything from the refactored source
2
+ export * from './src/index';
2
3
 
3
- // Type utilities for extracting types from Zod schemas
4
- type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
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 options interface for setting cookies
19
- interface CookieOptions {
20
- domain?: string;
21
- path?: string;
22
- expires?: Date;
23
- maxAge?: number;
24
- secure?: boolean;
25
- httpOnly?: boolean;
26
- sameSite?: 'Strict' | 'Lax' | 'None';
27
- }
28
-
29
- // OpenAPI detail information
30
- interface RouteDetail {
31
- summary?: string;
32
- description?: string;
33
- tags?: string[];
34
- operationId?: string;
35
- deprecated?: boolean;
36
- produces?: string[];
37
- consumes?: string[];
38
- [key: string]: any; // Allow additional OpenAPI properties
39
- }
40
-
41
- // Configuration interface for route handlers
42
- interface RouteConfig {
43
- params?: z.ZodSchema<any>;
44
- query?: z.ZodSchema<any>;
45
- body?: z.ZodSchema<any>;
46
- headers?: z.ZodSchema<any>;
47
- cookies?: z.ZodSchema<any>;
48
- response?: ResponseConfig;
49
- detail?: RouteDetail;
50
- }
51
-
52
- // Helper type to extract status codes from response config
53
- type StatusCodes<T> = T extends Record<number, any> ? keyof T : never;
54
-
55
- // Context type that's fully typed based on the route configuration
56
- export type Context<TConfig extends RouteConfig = {}> = {
57
- params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
58
- query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
59
- body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
60
- headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
61
- cookies: TConfig['cookies'] extends z.ZodSchema<any> ? InferZodType<TConfig['cookies']> : Record<string, string>;
62
- path: string;
63
- request: Request;
64
- set: {
65
- status: number;
66
- headers: Record<string, string>;
67
- cookies: (name: string, value: string, options?: CookieOptions) => void;
68
- redirect?: { location: string; status?: number };
69
- };
70
- status: <T extends number>(
71
- code: TConfig['response'] extends StatusResponseSchema
72
- ? StatusCodes<TConfig['response']> | number
73
- : T,
74
- data?: TConfig['response'] extends StatusResponseSchema
75
- ? T extends keyof TConfig['response']
76
- ? InferZodType<TConfig['response'][T]>
77
- : any
78
- : TConfig['response'] extends ResponseSchema
79
- ? InferZodType<TConfig['response']>
80
- : any
81
- ) => TConfig['response'] extends StatusResponseSchema
82
- ? T extends keyof TConfig['response']
83
- ? InferZodType<TConfig['response'][T]>
84
- : any
85
- : TConfig['response'] extends ResponseSchema
86
- ? InferZodType<TConfig['response']>
87
- : any;
88
- redirect: (location: string, status?: number) => Response;
89
- clearRedirect: () => void;
90
- [key: string]: any;
91
- };
92
-
93
- // Internal cookie storage interface
94
- interface InternalCookie {
95
- name: string;
96
- value: string;
97
- domain?: string;
98
- path?: string;
99
- expires?: Date;
100
- maxAge?: number;
101
- secure?: boolean;
102
- httpOnly?: boolean;
103
- sameSite?: 'Strict' | 'Lax' | 'None';
104
- }
105
-
106
- // Handler function type with proper response typing
107
- type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
108
-
109
- // Route definition
110
- interface Route {
111
- method: string;
112
- path: string;
113
- handler: Handler<any>;
114
- config?: RouteConfig;
115
- }
116
-
117
- // WebSocket handler interface
118
- interface WebSocketHandler {
119
- onOpen?: (ws: any) => void;
120
- onMessage?: (ws: any, message: string | Buffer) => void;
121
- onClose?: (ws: any, code?: number, reason?: string) => void;
122
- onError?: (ws: any, error: Error) => void;
123
- }
124
-
125
- // WebSocket route definition
126
- interface WSRoute {
127
- path: string;
128
- handler: WebSocketHandler;
129
- }
130
-
131
- // Lifecycle hooks
132
- interface LifecycleHooks {
133
- onBeforeStart?: (instance: BXO) => Promise<void> | void;
134
- onAfterStart?: (instance: BXO) => Promise<void> | void;
135
- onBeforeStop?: (instance: BXO) => Promise<void> | void;
136
- onAfterStop?: (instance: BXO) => Promise<void> | void;
137
- onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
138
- onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
139
- onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
140
- }
141
-
142
- // Add global options for BXO
143
- interface BXOOptions {
144
- enableValidation?: boolean;
145
- }
146
-
147
- // Plugin interface for middleware-style plugins
148
- interface Plugin {
149
- name?: string;
150
- onRequest?: (ctx: Context) => Promise<Response | void> | Response | void;
151
- onResponse?: (ctx: Context, response: any) => Promise<any> | any;
152
- onError?: (ctx: Context, error: Error) => Promise<any> | any;
153
- }
154
-
155
- export default class BXO {
156
- private _routes: Route[] = [];
157
- private _wsRoutes: WSRoute[] = [];
158
- private plugins: BXO[] = [];
159
- private middleware: Plugin[] = []; // New middleware array
160
- private hooks: {
161
- onBeforeStart?: (instance: BXO) => Promise<void> | void;
162
- onAfterStart?: (instance: BXO) => Promise<void> | void;
163
- onBeforeRestart?: (instance: BXO) => Promise<void> | void;
164
- onAfterRestart?: (instance: BXO) => Promise<void> | void;
165
- onBeforeStop?: (instance: BXO) => Promise<void> | void;
166
- onAfterStop?: (instance: BXO) => Promise<void> | void;
167
- onRequest?: (ctx: Context, instance: BXO) => Promise<Response | void> | Response | void;
168
- onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
169
- onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
170
- } = {};
171
- private server?: any;
172
- private isRunning: boolean = false;
173
- private serverPort?: number;
174
- private serverHostname?: string;
175
- private enableValidation: boolean = true;
176
-
177
- constructor(options?: BXOOptions) {
178
- this.enableValidation = options?.enableValidation ?? true;
179
- }
180
-
181
- // Lifecycle hook methods
182
- onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
183
- this.hooks.onBeforeStart = handler;
184
- return this;
185
- }
186
-
187
- onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
188
- this.hooks.onAfterStart = handler;
189
- return this;
190
- }
191
-
192
- onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
193
- this.hooks.onBeforeStop = handler;
194
- return this;
195
- }
196
-
197
- onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
198
- this.hooks.onAfterStop = handler;
199
- return this;
200
- }
201
-
202
-
203
-
204
- onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
205
- this.hooks.onRequest = handler;
206
- return this;
207
- }
208
-
209
- onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
210
- this.hooks.onResponse = handler;
211
- return this;
212
- }
213
-
214
- onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
215
- this.hooks.onError = handler;
216
- return this;
217
- }
218
-
219
- // Plugin system - now accepts both BXO instances and middleware plugins
220
- use(plugin: BXO | Plugin): this {
221
- if ('_routes' in plugin) {
222
- // It's a BXO instance
223
- this.plugins.push(plugin);
224
- } else {
225
- // It's a middleware plugin
226
- this.middleware.push(plugin);
227
- }
228
- return this;
229
- }
230
-
231
- // HTTP method handlers with overloads for type safety
232
- get<TConfig extends RouteConfig = {}>(
233
- path: string,
234
- handler: Handler<TConfig>
235
- ): this;
236
- get<TConfig extends RouteConfig = {}>(
237
- path: string,
238
- handler: Handler<TConfig>,
239
- config: TConfig
240
- ): this;
241
- get<TConfig extends RouteConfig = {}>(
242
- path: string,
243
- handler: Handler<TConfig>,
244
- config?: TConfig
245
- ): this {
246
- this._routes.push({ method: 'GET', path, handler, config });
247
- return this;
248
- }
249
-
250
- post<TConfig extends RouteConfig = {}>(
251
- path: string,
252
- handler: Handler<TConfig>
253
- ): this;
254
- post<TConfig extends RouteConfig = {}>(
255
- path: string,
256
- handler: Handler<TConfig>,
257
- config: TConfig
258
- ): this;
259
- post<TConfig extends RouteConfig = {}>(
260
- path: string,
261
- handler: Handler<TConfig>,
262
- config?: TConfig
263
- ): this {
264
- this._routes.push({ method: 'POST', path, handler, config });
265
- return this;
266
- }
267
-
268
- put<TConfig extends RouteConfig = {}>(
269
- path: string,
270
- handler: Handler<TConfig>
271
- ): this;
272
- put<TConfig extends RouteConfig = {}>(
273
- path: string,
274
- handler: Handler<TConfig>,
275
- config: TConfig
276
- ): this;
277
- put<TConfig extends RouteConfig = {}>(
278
- path: string,
279
- handler: Handler<TConfig>,
280
- config?: TConfig
281
- ): this {
282
- this._routes.push({ method: 'PUT', path, handler, config });
283
- return this;
284
- }
285
-
286
- delete<TConfig extends RouteConfig = {}>(
287
- path: string,
288
- handler: Handler<TConfig>
289
- ): this;
290
- delete<TConfig extends RouteConfig = {}>(
291
- path: string,
292
- handler: Handler<TConfig>,
293
- config: TConfig
294
- ): this;
295
- delete<TConfig extends RouteConfig = {}>(
296
- path: string,
297
- handler: Handler<TConfig>,
298
- config?: TConfig
299
- ): this {
300
- this._routes.push({ method: 'DELETE', path, handler, config });
301
- return this;
302
- }
303
-
304
- patch<TConfig extends RouteConfig = {}>(
305
- path: string,
306
- handler: Handler<TConfig>
307
- ): this;
308
- patch<TConfig extends RouteConfig = {}>(
309
- path: string,
310
- handler: Handler<TConfig>,
311
- config: TConfig
312
- ): this;
313
- patch<TConfig extends RouteConfig = {}>(
314
- path: string,
315
- handler: Handler<TConfig>,
316
- config?: TConfig
317
- ): this {
318
- this._routes.push({ method: 'PATCH', path, handler, config });
319
- return this;
320
- }
321
-
322
- // WebSocket route handler
323
- ws(path: string, handler: WebSocketHandler): this {
324
- this._wsRoutes.push({ path, handler });
325
- return this;
326
- }
327
-
328
- // Helper methods to get all routes including plugin routes
329
- private getAllRoutes(): Route[] {
330
- const allRoutes = [...this._routes];
331
- for (const plugin of this.plugins) {
332
- allRoutes.push(...plugin._routes);
333
- }
334
- return allRoutes;
335
- }
336
-
337
- private getAllWSRoutes(): WSRoute[] {
338
- const allWSRoutes = [...this._wsRoutes];
339
- for (const plugin of this.plugins) {
340
- allWSRoutes.push(...plugin._wsRoutes);
341
- }
342
- return allWSRoutes;
343
- }
344
-
345
- // Route matching utility
346
- private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
347
- const allRoutes = this.getAllRoutes();
348
-
349
- for (const route of allRoutes) {
350
- if (route.method !== method) continue;
351
-
352
- const routeSegments = route.path.split('/').filter(Boolean);
353
- const pathSegments = pathname.split('/').filter(Boolean);
354
-
355
- const params: Record<string, string> = {};
356
- let isMatch = true;
357
-
358
- // Check for double wildcard (**) in the route
359
- const hasDoubleWildcard = routeSegments.some(segment => segment === '**');
360
-
361
- // Handle double wildcard at the end (catch-all with slashes)
362
- const hasDoubleWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '**';
363
-
364
- // Handle single wildcard at the end (catch-all)
365
- const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
366
-
367
- if (hasDoubleWildcardAtEnd) {
368
- // For double wildcard at end, path must have at least as many segments as route (minus the double wildcard)
369
- if (pathSegments.length < routeSegments.length - 1) continue;
370
- } else if (hasWildcardAtEnd) {
371
- // For single wildcard at end, path must have at least as many segments as route (minus the wildcard)
372
- if (pathSegments.length < routeSegments.length - 1) continue;
373
- } else if (!hasDoubleWildcard) {
374
- // For exact matching (with possible single-segment wildcards), lengths must match
375
- if (routeSegments.length !== pathSegments.length) continue;
376
- }
377
-
378
- for (let i = 0; i < routeSegments.length; i++) {
379
- const routeSegment = routeSegments[i];
380
- const pathSegment = pathSegments[i];
381
-
382
- if (!routeSegment) {
383
- isMatch = false;
384
- break;
385
- }
386
-
387
- // Handle double wildcard at the end (matches everything including slashes)
388
- if (routeSegment === '**' && i === routeSegments.length - 1) {
389
- const remainingPath = pathSegments.slice(i).join('/');
390
- params['**'] = remainingPath;
391
- break;
392
- }
393
-
394
- // Handle single wildcard at the end (catch-all)
395
- if (routeSegment === '*' && i === routeSegments.length - 1) {
396
- // Wildcard at end matches remaining path segments
397
- const remainingPath = pathSegments.slice(i).join('/');
398
- params['*'] = remainingPath;
399
- break;
400
- }
401
-
402
- // Handle double wildcard in the middle (matches everything including slashes)
403
- if (routeSegment === '**') {
404
- // Find the next non-wildcard segment to match against
405
- let nextNonWildcardIndex = i + 1;
406
- while (nextNonWildcardIndex < routeSegments.length &&
407
- (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
408
- nextNonWildcardIndex++;
409
- }
410
-
411
- if (nextNonWildcardIndex >= routeSegments.length) {
412
- // Double wildcard is at the end or followed by other wildcards
413
- const remainingPath = pathSegments.slice(i).join('/');
414
- params['**'] = remainingPath;
415
- break;
416
- }
417
-
418
- // Find the next matching segment in the path
419
- const nextRouteSegment = routeSegments[nextNonWildcardIndex];
420
- if (!nextRouteSegment) {
421
- isMatch = false;
422
- break;
423
- }
424
-
425
- let foundMatch = false;
426
- let matchedPath = '';
427
-
428
- for (let j = i; j < pathSegments.length; j++) {
429
- const currentPathSegment = pathSegments[j];
430
-
431
- // Check if this path segment matches the next route segment
432
- if (nextRouteSegment.startsWith(':')) {
433
- // Param segment - always matches
434
- matchedPath = pathSegments.slice(i, j).join('/');
435
- params['**'] = matchedPath;
436
- i = j - 1; // Adjust index for the next iteration
437
- foundMatch = true;
438
- break;
439
- } else if (nextRouteSegment === '*') {
440
- // Single wildcard - always matches
441
- matchedPath = pathSegments.slice(i, j).join('/');
442
- params['**'] = matchedPath;
443
- i = j - 1; // Adjust index for the next iteration
444
- foundMatch = true;
445
- break;
446
- } else if (nextRouteSegment === currentPathSegment) {
447
- // Exact match
448
- matchedPath = pathSegments.slice(i, j).join('/');
449
- params['**'] = matchedPath;
450
- i = j - 1; // Adjust index for the next iteration
451
- foundMatch = true;
452
- break;
453
- }
454
- }
455
-
456
- if (!foundMatch) {
457
- isMatch = false;
458
- break;
459
- }
460
-
461
- continue;
462
- }
463
-
464
- if (!pathSegment) {
465
- isMatch = false;
466
- break;
467
- }
468
-
469
- if (routeSegment.startsWith(':')) {
470
- const paramName = routeSegment.slice(1);
471
- params[paramName] = pathSegment;
472
- } else if (routeSegment === '*') {
473
- // Single segment wildcard
474
- params['*'] = pathSegment;
475
- } else if (routeSegment !== pathSegment) {
476
- isMatch = false;
477
- break;
478
- }
479
- }
480
-
481
- if (isMatch) {
482
- return { route, params };
483
- }
484
- }
485
-
486
- return null;
487
- }
488
-
489
- // WebSocket route matching utility
490
- private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
491
- const allWSRoutes = this.getAllWSRoutes();
492
-
493
- for (const route of allWSRoutes) {
494
- const routeSegments = route.path.split('/').filter(Boolean);
495
- const pathSegments = pathname.split('/').filter(Boolean);
496
-
497
- const params: Record<string, string> = {};
498
- let isMatch = true;
499
-
500
- // Check for double wildcard (**) in the route
501
- const hasDoubleWildcard = routeSegments.some(segment => segment === '**');
502
-
503
- // Handle double wildcard at the end (catch-all with slashes)
504
- const hasDoubleWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '**';
505
-
506
- // Handle single wildcard at the end (catch-all)
507
- const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
508
-
509
- if (hasDoubleWildcardAtEnd) {
510
- // For double wildcard at end, path must have at least as many segments as route (minus the double wildcard)
511
- if (pathSegments.length < routeSegments.length - 1) continue;
512
- } else if (hasWildcardAtEnd) {
513
- // For single wildcard at end, path must have at least as many segments as route (minus the wildcard)
514
- if (pathSegments.length < routeSegments.length - 1) continue;
515
- } else if (!hasDoubleWildcard) {
516
- // For exact matching (with possible single-segment wildcards), lengths must match
517
- if (routeSegments.length !== pathSegments.length) continue;
518
- }
519
-
520
- for (let i = 0; i < routeSegments.length; i++) {
521
- const routeSegment = routeSegments[i];
522
- const pathSegment = pathSegments[i];
523
-
524
- if (!routeSegment) {
525
- isMatch = false;
526
- break;
527
- }
528
-
529
- // Handle double wildcard at the end (matches everything including slashes)
530
- if (routeSegment === '**' && i === routeSegments.length - 1) {
531
- const remainingPath = pathSegments.slice(i).join('/');
532
- params['**'] = remainingPath;
533
- break;
534
- }
535
-
536
- // Handle single wildcard at the end (catch-all)
537
- if (routeSegment === '*' && i === routeSegments.length - 1) {
538
- // Wildcard at end matches remaining path segments
539
- const remainingPath = pathSegments.slice(i).join('/');
540
- params['*'] = remainingPath;
541
- break;
542
- }
543
-
544
- // Handle double wildcard in the middle (matches everything including slashes)
545
- if (routeSegment === '**') {
546
- // Find the next non-wildcard segment to match against
547
- let nextNonWildcardIndex = i + 1;
548
- while (nextNonWildcardIndex < routeSegments.length &&
549
- (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
550
- nextNonWildcardIndex++;
551
- }
552
-
553
- if (nextNonWildcardIndex >= routeSegments.length) {
554
- // Double wildcard is at the end or followed by other wildcards
555
- const remainingPath = pathSegments.slice(i).join('/');
556
- params['**'] = remainingPath;
557
- break;
558
- }
559
-
560
- // Find the next matching segment in the path
561
- const nextRouteSegment = routeSegments[nextNonWildcardIndex];
562
- if (!nextRouteSegment) {
563
- isMatch = false;
564
- break;
565
- }
566
-
567
- let foundMatch = false;
568
- let matchedPath = '';
569
-
570
- for (let j = i; j < pathSegments.length; j++) {
571
- const currentPathSegment = pathSegments[j];
572
-
573
- // Check if this path segment matches the next route segment
574
- if (nextRouteSegment.startsWith(':')) {
575
- // Param segment - always matches
576
- matchedPath = pathSegments.slice(i, j).join('/');
577
- params['**'] = matchedPath;
578
- i = j - 1; // Adjust index for the next iteration
579
- foundMatch = true;
580
- break;
581
- } else if (nextRouteSegment === '*') {
582
- // Single wildcard - always matches
583
- matchedPath = pathSegments.slice(i, j).join('/');
584
- params['**'] = matchedPath;
585
- i = j - 1; // Adjust index for the next iteration
586
- foundMatch = true;
587
- break;
588
- } else if (nextRouteSegment === currentPathSegment) {
589
- // Exact match
590
- matchedPath = pathSegments.slice(i, j).join('/');
591
- params['**'] = matchedPath;
592
- i = j - 1; // Adjust index for the next iteration
593
- foundMatch = true;
594
- break;
595
- }
596
- }
597
-
598
- if (!foundMatch) {
599
- isMatch = false;
600
- break;
601
- }
602
-
603
- continue;
604
- }
605
-
606
- if (!pathSegment) {
607
- isMatch = false;
608
- break;
609
- }
610
-
611
- if (routeSegment.startsWith(':')) {
612
- const paramName = routeSegment.slice(1);
613
- params[paramName] = pathSegment;
614
- } else if (routeSegment === '*') {
615
- // Single segment wildcard
616
- params['*'] = pathSegment;
617
- } else if (routeSegment !== pathSegment) {
618
- isMatch = false;
619
- break;
620
- }
621
- }
622
-
623
- if (isMatch) {
624
- return { route, params };
625
- }
626
- }
627
-
628
- return null;
629
- }
630
-
631
- // Parse query string
632
- private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
633
- const query: Record<string, string | undefined> = {};
634
- searchParams.forEach((value, key) => {
635
- query[key] = value;
636
- });
637
- return query;
638
- }
639
-
640
- // Parse headers
641
- private parseHeaders(headers: Headers): Record<string, string> {
642
- const headerObj: Record<string, string> = {};
643
- headers.forEach((value, key) => {
644
- headerObj[key] = value;
645
- });
646
- return headerObj;
647
- }
648
-
649
- // Parse cookies from Cookie header
650
- private parseCookies(cookieHeader: string | null): Record<string, string> {
651
- const cookies: Record<string, string> = {};
652
-
653
- if (!cookieHeader) return cookies;
654
-
655
- const cookiePairs = cookieHeader.split(';');
656
- for (const pair of cookiePairs) {
657
- const [name, value] = pair.trim().split('=');
658
- if (name && value) {
659
- cookies[decodeURIComponent(name)] = decodeURIComponent(value);
660
- }
661
- }
662
-
663
- return cookies;
664
- }
665
-
666
- // Validate data against Zod schema
667
- private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
668
- if (!schema) return data;
669
- return schema.parse(data);
670
- }
671
-
672
- // Validate response against response config (supports both simple and status-based schemas)
673
- private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
674
- if (!responseConfig || !this.enableValidation) return data;
675
-
676
- // If it's a simple schema (not status-based)
677
- if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
678
- return responseConfig.parse(data);
679
- }
680
-
681
- // If it's a status-based schema
682
- if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
683
- const statusSchema = responseConfig[status];
684
- if (statusSchema) {
685
- return statusSchema.parse(data);
686
- }
687
-
688
- // If no specific status schema found, try to find a fallback
689
- // Common fallback statuses: 200, 201, 400, 500
690
- const fallbackStatuses = [200, 201, 400, 500];
691
- for (const fallbackStatus of fallbackStatuses) {
692
- if (responseConfig[fallbackStatus]) {
693
- return responseConfig[fallbackStatus]?.parse(data);
694
- }
695
- }
696
-
697
- // If no schema found for the status, return data as-is
698
- return data;
699
- }
700
-
701
- return data;
702
- }
703
-
704
- // Main request handler
705
- private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
706
- const url = new URL(request.url);
707
- const method = request.method;
708
- const rawPathname = url.pathname;
709
- let pathname: string;
710
- try {
711
- pathname = decodeURI(rawPathname);
712
- } catch {
713
- pathname = rawPathname;
714
- }
715
-
716
- // Check for WebSocket upgrade
717
- if (request.headers.get('upgrade') === 'websocket') {
718
- const wsMatchResult = this.matchWSRoute(pathname);
719
- if (wsMatchResult && server) {
720
- const success = server.upgrade(request, {
721
- data: {
722
- handler: wsMatchResult.route.handler,
723
- params: wsMatchResult.params,
724
- pathname
725
- }
726
- });
727
-
728
- if (success) {
729
- return; // undefined response means upgrade was successful
730
- }
731
- }
732
- return new Response('WebSocket upgrade failed', { status: 400 });
733
- }
734
-
735
- // Handle OPTIONS requests for CORS preflight before route matching
736
- if (method === 'OPTIONS') {
737
- // Create a minimal context for OPTIONS requests
738
- const ctx: Context = {
739
- params: {},
740
- query: {},
741
- body: {},
742
- headers: this.parseHeaders(request.headers),
743
- cookies: {},
744
- path: pathname,
745
- request,
746
- set: {
747
- status: 200,
748
- headers: {},
749
- cookies: (name: string, value: string, options?: CookieOptions) => {
750
- // This is a placeholder for setting cookies.
751
- // In a real Bun.serve context, you'd use Bun.serve's cookie handling.
752
- // For now, we'll just log it or throw an error if not Bun.serve.
753
- console.warn(`Setting cookie '${name}' with value '${value}' via ctx.set.cookies is not directly supported by Bun.serve. Use Bun.serve's cookie handling.`);
754
- },
755
- redirect: undefined
756
- },
757
- status: ((code: number, data?: any) => {
758
- ctx.set.status = code;
759
- return data;
760
- }) as any,
761
- redirect: ((location: string, status: number = 302) => {
762
- ctx.set.redirect = { location, status };
763
- const responseHeaders: Record<string, string> = {
764
- Location: location,
765
- ...(ctx.set.headers || {})
766
- };
767
- return new Response(null, {
768
- status,
769
- headers: responseHeaders
770
- });
771
- }) as any,
772
- clearRedirect: (() => {
773
- delete ctx.set.redirect;
774
- if (ctx.set.headers) {
775
- for (const key of Object.keys(ctx.set.headers)) {
776
- if (key.toLowerCase() === 'location') {
777
- delete ctx.set.headers[key];
778
- }
779
- }
780
- }
781
- if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
782
- ctx.set.status = 200;
783
- }
784
- }) as any
785
- };
786
-
787
- // Run middleware onRequest hooks for OPTIONS requests
788
- for (const plugin of this.middleware) {
789
- if (plugin.onRequest) {
790
- const result = await plugin.onRequest(ctx);
791
- // If middleware returns a response, return it immediately
792
- if (result instanceof Response) {
793
- return result;
794
- }
795
- }
796
- }
797
-
798
- // Run global onRequest hook
799
- if (this.hooks.onRequest) {
800
- const result = await this.hooks.onRequest(ctx, this);
801
- if (result instanceof Response) {
802
- return result;
803
- }
804
- }
805
-
806
- // Run BXO instance onRequest hooks
807
- for (const bxoInstance of this.plugins) {
808
- if (bxoInstance.hooks.onRequest) {
809
- const result = await bxoInstance.hooks.onRequest(ctx, this);
810
- if (result instanceof Response) {
811
- return result;
812
- }
813
- }
814
- }
815
-
816
- // If no middleware handled the OPTIONS request, return a default response
817
- return new Response(null, {
818
- status: 204,
819
- headers: ctx.set.headers || {}
820
- });
821
- }
822
-
823
- const matchResult = this.matchRoute(method, pathname);
824
- if (!matchResult) {
825
- return new Response('Not Found', { status: 404 });
826
- }
827
-
828
- const { route, params } = matchResult;
829
- const query = this.parseQuery(url.searchParams);
830
- const headers = this.parseHeaders(request.headers);
831
- const cookies = this.parseCookies(request.headers.get('cookie'));
832
-
833
- let body: any;
834
- if (request.method !== 'GET' && request.method !== 'HEAD') {
835
- const contentType = request.headers.get('content-type');
836
- if (contentType?.includes('application/json')) {
837
- try {
838
- body = await request.json();
839
- } catch {
840
- body = {};
841
- }
842
- } else if (contentType?.includes('application/x-www-form-urlencoded')) {
843
- const formData = await request.formData();
844
- body = Object.fromEntries(formData.entries());
845
- } else {
846
- // Try to parse as JSON if it looks like JSON, otherwise treat as text
847
- const textBody = await request.text();
848
- try {
849
- // Check if the text looks like JSON
850
- if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
851
- body = JSON.parse(textBody);
852
- } else {
853
- body = textBody;
854
- }
855
- } catch {
856
- body = textBody;
857
- }
858
- }
859
- }
860
-
861
- // Create internal cookie storage
862
- const internalCookies: InternalCookie[] = [];
863
-
864
- // Create context with validation
865
- let ctx: Context;
866
- try {
867
- // Validate each part separately to get better error messages
868
- const validatedParams = this.enableValidation && route.config?.params ? this.validateData(route.config.params, params) : params;
869
- const validatedQuery = this.enableValidation && route.config?.query ? this.validateData(route.config.query, query) : query;
870
- const validatedBody = this.enableValidation && route.config?.body ? this.validateData(route.config.body, body) : body;
871
- const validatedHeaders = this.enableValidation && route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
872
- const validatedCookies = this.enableValidation && route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
873
-
874
- ctx = {
875
- params: validatedParams,
876
- query: validatedQuery,
877
- body: validatedBody,
878
- headers: validatedHeaders,
879
- cookies: validatedCookies,
880
- path: pathname,
881
- request,
882
- set: {
883
- status: 200,
884
- headers: {},
885
- cookies: (name: string, value: string, options?: CookieOptions) => {
886
- internalCookies.push({
887
- name,
888
- value,
889
- domain: options?.domain,
890
- path: options?.path,
891
- expires: options?.expires,
892
- maxAge: options?.maxAge,
893
- secure: options?.secure,
894
- httpOnly: options?.httpOnly,
895
- sameSite: options?.sameSite
896
- });
897
- }
898
- },
899
- status: ((code: number, data?: any) => {
900
- ctx.set.status = code;
901
- return data;
902
- }) as any,
903
- redirect: ((location: string, status: number = 302) => {
904
- // Record redirect intent only; avoid mutating generic status/headers so it can be canceled later
905
- ctx.set.redirect = { location, status };
906
-
907
- // Prepare headers for immediate Response return without persisting to ctx.set.headers
908
- const responseHeaders = new Headers();
909
- responseHeaders.set('Location', location);
910
-
911
- // Add any additional headers from ctx.set.headers
912
- if (ctx.set.headers) {
913
- Object.entries(ctx.set.headers).forEach(([key, value]) => {
914
- responseHeaders.set(key, value);
915
- });
916
- }
917
-
918
- // Handle cookies if any are set on context
919
- if (internalCookies.length > 0) {
920
- const cookieHeaders = internalCookies.map(cookie => {
921
- let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
922
- if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
923
- if (cookie.path) cookieString += `; Path=${cookie.path}`;
924
- if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
925
- if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
926
- if (cookie.secure) cookieString += `; Secure`;
927
- if (cookie.httpOnly) cookieString += `; HttpOnly`;
928
- if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
929
- return cookieString;
930
- });
931
- // Set multiple Set-Cookie headers properly
932
- cookieHeaders.forEach(cookieHeader => {
933
- responseHeaders.append('Set-Cookie', cookieHeader);
934
- });
935
- }
936
-
937
- return new Response(null, {
938
- status,
939
- headers: responseHeaders
940
- });
941
- }) as any,
942
- clearRedirect: (() => {
943
- // Clear explicit redirect intent
944
- delete ctx.set.redirect;
945
- // Remove any Location header if present
946
- if (ctx.set.headers) {
947
- for (const key of Object.keys(ctx.set.headers)) {
948
- if (key.toLowerCase() === 'location') {
949
- delete ctx.set.headers[key];
950
- }
951
- }
952
- }
953
- // Reset status if it is a redirect
954
- if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
955
- ctx.set.status = 200;
956
- }
957
- }) as any
958
- };
959
- } catch (validationError) {
960
- // Validation failed - return error response
961
-
962
- // Extract detailed validation errors from Zod
963
- let validationDetails = undefined;
964
- if (validationError instanceof Error) {
965
- if ('errors' in validationError && Array.isArray(validationError.errors)) {
966
- validationDetails = validationError.errors;
967
- } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
968
- validationDetails = validationError.issues;
969
- }
970
- }
971
-
972
- // Create a clean error message
973
- const errorMessage = validationDetails && validationDetails.length > 0
974
- ? `Validation failed for ${validationDetails.length} field(s)`
975
- : 'Validation failed';
976
-
977
- return new Response(JSON.stringify({
978
- error: errorMessage,
979
- details: validationDetails
980
- }), {
981
- status: 400,
982
- headers: { 'Content-Type': 'application/json' }
983
- });
984
- }
985
-
986
- try {
987
- // Run middleware onRequest hooks
988
- for (const plugin of this.middleware) {
989
- if (plugin.onRequest) {
990
- await plugin.onRequest(ctx);
991
- }
992
- }
993
-
994
- // Run global onRequest hook
995
- if (this.hooks.onRequest) {
996
- await this.hooks.onRequest(ctx, this);
997
- }
998
-
999
- // Run BXO instance onRequest hooks
1000
- for (const bxoInstance of this.plugins) {
1001
- if (bxoInstance.hooks.onRequest) {
1002
- await bxoInstance.hooks.onRequest(ctx, this);
1003
- }
1004
- }
1005
-
1006
- // Execute route handler
1007
- let response = await route.handler(ctx);
1008
-
1009
- // Run middleware onResponse hooks
1010
- for (const plugin of this.middleware) {
1011
- if (plugin.onResponse) {
1012
- response = await plugin.onResponse(ctx, response) || response;
1013
- }
1014
- }
1015
-
1016
- // Run global onResponse hook
1017
- if (this.hooks.onResponse) {
1018
- response = await this.hooks.onResponse(ctx, response, this) || response;
1019
- }
1020
-
1021
- // Run BXO instance onResponse hooks
1022
- for (const bxoInstance of this.plugins) {
1023
- if (bxoInstance.hooks.onResponse) {
1024
- response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
1025
- }
1026
- }
1027
-
1028
- // If the handler did not return a response, but a redirect was configured via ctx.set,
1029
- // automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
1030
- const hasImplicitRedirectIntent = !!ctx.set.redirect
1031
- || (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400);
1032
- if ((response === undefined || response === null) && hasImplicitRedirectIntent) {
1033
- const locationFromHeaders = ctx.set.headers && Object.entries(ctx.set.headers).find(([k]) => k.toLowerCase() === 'location')?.[1];
1034
- const location = ctx.set.redirect?.location || locationFromHeaders;
1035
- if (location) {
1036
- // Build headers, ensuring Location is present
1037
- let responseHeaders: Record<string, string> = ctx.set.headers ? { ...ctx.set.headers } : {};
1038
- if (!Object.keys(responseHeaders).some(k => k.toLowerCase() === 'location')) {
1039
- responseHeaders['Location'] = location;
1040
- }
1041
- // Determine status precedence: redirect.status > set.status > 302
1042
- const status = ctx.set.redirect?.status ?? ctx.set.status ?? 302;
1043
-
1044
- // Handle cookies if any are set
1045
- if (internalCookies.length > 0) {
1046
- const cookieHeaders = internalCookies.map(cookie => {
1047
- let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1048
- if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1049
- if (cookie.path) cookieString += `; Path=${cookie.path}`;
1050
- if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
1051
- if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
1052
- if (cookie.secure) cookieString += `; Secure`;
1053
- if (cookie.httpOnly) cookieString += `; HttpOnly`;
1054
- if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1055
- return cookieString;
1056
- });
1057
- // Convert responseHeaders to Headers object for proper multiple Set-Cookie handling
1058
- const headers = new Headers();
1059
- Object.entries(responseHeaders).forEach(([key, value]) => {
1060
- headers.set(key, value);
1061
- });
1062
- cookieHeaders.forEach(cookieHeader => {
1063
- headers.append('Set-Cookie', cookieHeader);
1064
- });
1065
- // Convert back to plain object for Response constructor
1066
- const finalHeaders: Record<string, string> = {};
1067
- headers.forEach((value, key) => {
1068
- finalHeaders[key] = value;
1069
- });
1070
- responseHeaders = finalHeaders;
1071
- }
1072
-
1073
- return new Response(null, {
1074
- status,
1075
- headers: responseHeaders
1076
- });
1077
- }
1078
- }
1079
-
1080
- // Validate response against schema if provided
1081
- if (this.enableValidation && route.config?.response && !(response instanceof Response)) {
1082
- try {
1083
- const status = ctx.set.status || 200;
1084
- response = this.validateResponse(route.config.response, response, status);
1085
- } catch (validationError) {
1086
- // Response validation failed
1087
-
1088
- // Extract detailed validation errors from Zod
1089
- let validationDetails = undefined;
1090
- if (validationError instanceof Error) {
1091
- if ('errors' in validationError && Array.isArray(validationError.errors)) {
1092
- validationDetails = validationError.errors;
1093
- } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
1094
- validationDetails = validationError.issues;
1095
- }
1096
- }
1097
-
1098
- // Create a clean error message
1099
- const errorMessage = validationDetails && validationDetails.length > 0
1100
- ? `Response validation failed for ${validationDetails.length} field(s)`
1101
- : 'Response validation failed';
1102
-
1103
- return new Response(JSON.stringify({
1104
- error: errorMessage,
1105
- details: validationDetails
1106
- }), {
1107
- status: 500,
1108
- headers: { 'Content-Type': 'application/json' }
1109
- });
1110
- }
1111
- }
1112
-
1113
- // Convert response to Response object
1114
- if (response instanceof Response) {
1115
- // If there are headers set via ctx.set.headers, merge them with the Response headers
1116
- if (ctx.set.headers && Object.keys(ctx.set.headers).length > 0) {
1117
- const newHeaders = new Headers(response.headers);
1118
-
1119
- // Add headers from ctx.set.headers
1120
- Object.entries(ctx.set.headers).forEach(([key, value]) => {
1121
- newHeaders.set(key, value);
1122
- });
1123
-
1124
- // Handle cookies if any are set
1125
- if (internalCookies.length > 0) {
1126
- const cookieHeaders = internalCookies.map(cookie => {
1127
- let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1128
- if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1129
- if (cookie.path) cookieString += `; Path=${cookie.path}`;
1130
- if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
1131
- if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
1132
- if (cookie.secure) cookieString += `; Secure`;
1133
- if (cookie.httpOnly) cookieString += `; HttpOnly`;
1134
- if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1135
- return cookieString;
1136
- });
1137
-
1138
- // Add Set-Cookie headers
1139
- cookieHeaders.forEach(cookieHeader => {
1140
- newHeaders.append('Set-Cookie', cookieHeader);
1141
- });
1142
- }
1143
-
1144
- // Create new Response with merged headers
1145
- return new Response(response.body, {
1146
- status: ctx.set.status || response.status,
1147
- statusText: response.statusText,
1148
- headers: newHeaders
1149
- });
1150
- }
1151
-
1152
- return response;
1153
- }
1154
-
1155
- // Handle File response (like Elysia)
1156
- if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
1157
- const file = response as File;
1158
- const responseInit: ResponseInit = {
1159
- status: ctx.set.status || 200,
1160
- headers: {
1161
- 'Content-Type': file.type || 'application/octet-stream',
1162
- 'Content-Length': file.size.toString(),
1163
- ...ctx.set.headers
1164
- }
1165
- };
1166
- return new Response(file, responseInit);
1167
- }
1168
-
1169
- // Handle Bun.file() response
1170
- if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
1171
- const bunFile = response as any;
1172
- const responseInit: ResponseInit = {
1173
- status: ctx.set.status || 200,
1174
- headers: {
1175
- 'Content-Type': bunFile.type || 'application/octet-stream',
1176
- 'Content-Length': bunFile.size?.toString() || '',
1177
- ...ctx.set.headers,
1178
- ...(bunFile.headers || {}) // Support custom headers from file helper
1179
- }
1180
- };
1181
- return new Response(bunFile, responseInit);
1182
- }
1183
-
1184
- // Prepare headers with cookies
1185
- let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
1186
-
1187
- // Handle cookies if any are set
1188
- if (internalCookies.length > 0) {
1189
- const cookieHeaders = internalCookies.map(cookie => {
1190
- let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1191
-
1192
- if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1193
- if (cookie.path) cookieString += `; Path=${cookie.path}`;
1194
- if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
1195
- if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
1196
- if (cookie.secure) cookieString += `; Secure`;
1197
- if (cookie.httpOnly) cookieString += `; HttpOnly`;
1198
- if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1199
-
1200
- return cookieString;
1201
- });
1202
-
1203
- // Add Set-Cookie headers
1204
- // Use Headers object directly for Response constructor to handle multiple Set-Cookie headers properly
1205
- const headers = new Headers();
1206
- Object.entries(responseHeaders).forEach(([key, value]) => {
1207
- headers.set(key, value);
1208
- });
1209
- cookieHeaders.forEach(cookieHeader => {
1210
- headers.append('Set-Cookie', cookieHeader);
1211
- });
1212
-
1213
- const responseInit: ResponseInit = {
1214
- status: ctx.set.status || 200,
1215
- headers: headers
1216
- };
1217
-
1218
- if (typeof response === 'string') {
1219
- return new Response(response, responseInit);
1220
- }
1221
-
1222
- return new Response(JSON.stringify(response), {
1223
- status: responseInit.status,
1224
- headers: headers
1225
- });
1226
- }
1227
-
1228
- // If no cookies, use the original responseHeaders
1229
- const responseInit: ResponseInit = {
1230
- status: ctx.set.status || 200,
1231
- headers: responseHeaders
1232
- };
1233
-
1234
- if (typeof response === 'string') {
1235
- return new Response(response, responseInit);
1236
- }
1237
-
1238
- return new Response(JSON.stringify(response), {
1239
- ...responseInit,
1240
- headers: {
1241
- 'Content-Type': 'application/json',
1242
- ...responseInit.headers
1243
- }
1244
- });
1245
-
1246
- } catch (error) {
1247
- // Run error hooks
1248
- let errorResponse: any;
1249
-
1250
- // Run middleware onError hooks
1251
- for (const plugin of this.middleware) {
1252
- if (plugin.onError) {
1253
- errorResponse = await plugin.onError(ctx, error as Error) || errorResponse;
1254
- }
1255
- }
1256
-
1257
- if (this.hooks.onError) {
1258
- errorResponse = await this.hooks.onError(ctx, error as Error, this);
1259
- }
1260
-
1261
- for (const bxoInstance of this.plugins) {
1262
- if (bxoInstance.hooks.onError) {
1263
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
1264
- }
1265
- }
1266
-
1267
- if (errorResponse) {
1268
- if (errorResponse instanceof Response) {
1269
- return errorResponse;
1270
- }
1271
- return new Response(JSON.stringify(errorResponse), {
1272
- status: 500,
1273
- headers: { 'Content-Type': 'application/json' }
1274
- });
1275
- }
1276
-
1277
- // Default error response
1278
- const errorMessage = error instanceof Error ? error.message : 'Internal Server Error';
1279
- return new Response(JSON.stringify({ error: errorMessage }), {
1280
- status: 500,
1281
- headers: { 'Content-Type': 'application/json' }
1282
- });
1283
- }
1284
- }
1285
-
1286
-
1287
-
1288
- // Server management methods
1289
- async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
1290
- if (this.isRunning) {
1291
- return;
1292
- }
1293
-
1294
- try {
1295
- // Before start hook
1296
- if (this.hooks.onBeforeStart) {
1297
- await this.hooks.onBeforeStart(this);
1298
- }
1299
-
1300
- this.server = Bun.serve({
1301
- port,
1302
- hostname,
1303
- fetch: (request, server) => this.handleRequest(request, server),
1304
- websocket: {
1305
- message: (ws: any, message: any) => {
1306
- const handler = ws.data?.handler;
1307
- if (handler?.onMessage) {
1308
- handler.onMessage(ws, message);
1309
- }
1310
- },
1311
- open: (ws: any) => {
1312
- const handler = ws.data?.handler;
1313
- if (handler?.onOpen) {
1314
- handler.onOpen(ws);
1315
- }
1316
- },
1317
- close: (ws: any, code?: number, reason?: string) => {
1318
- const handler = ws.data?.handler;
1319
- if (handler?.onClose) {
1320
- handler.onClose(ws, code, reason);
1321
- }
1322
- }
1323
- }
1324
- });
1325
-
1326
- // Verify server was created successfully
1327
- if (!this.server) {
1328
- throw new Error('Failed to create server instance');
1329
- }
1330
-
1331
- this.isRunning = true;
1332
- this.serverPort = port;
1333
- this.serverHostname = hostname;
1334
-
1335
- // After start hook
1336
- if (this.hooks.onAfterStart) {
1337
- await this.hooks.onAfterStart(this);
1338
- }
1339
-
1340
- // Handle graceful shutdown
1341
- const shutdownHandler = async () => {
1342
- await this.stop();
1343
- process.exit(0);
1344
- };
1345
-
1346
- process.on('SIGINT', shutdownHandler);
1347
- process.on('SIGTERM', shutdownHandler);
1348
-
1349
- } catch (error) {
1350
- console.error('❌ Failed to start server:', error);
1351
- throw error;
1352
- }
1353
- }
1354
-
1355
- async stop(): Promise<void> {
1356
- if (!this.isRunning) {
1357
- return;
1358
- }
1359
-
1360
- try {
1361
- // Before stop hook
1362
- if (this.hooks.onBeforeStop) {
1363
- await this.hooks.onBeforeStop(this);
1364
- }
1365
-
1366
- if (this.server) {
1367
- try {
1368
- // Try to stop the server gracefully
1369
- if (typeof this.server.stop === 'function') {
1370
- this.server.stop();
1371
- } else {
1372
- console.warn('⚠️ Server stop method not available');
1373
- }
1374
- } catch (stopError) {
1375
- console.error('❌ Error calling server.stop():', stopError);
1376
- }
1377
-
1378
- // Clear the server reference
1379
- this.server = undefined;
1380
- }
1381
-
1382
- // Reset state regardless of server.stop() success
1383
- this.isRunning = false;
1384
- this.serverPort = undefined;
1385
- this.serverHostname = undefined;
1386
-
1387
- // After stop hook
1388
- if (this.hooks.onAfterStop) {
1389
- await this.hooks.onAfterStop(this);
1390
- }
1391
-
1392
- } catch (error) {
1393
- console.error('❌ Error stopping server:', error);
1394
- // Even if there's an error, reset the state
1395
- this.isRunning = false;
1396
- this.server = undefined;
1397
- this.serverPort = undefined;
1398
- this.serverHostname = undefined;
1399
- throw error;
1400
- }
1401
- }
1402
-
1403
-
1404
-
1405
- // Backward compatibility
1406
- async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
1407
- return this.start(port, hostname);
1408
- }
1409
-
1410
- // Server status
1411
- isServerRunning(): boolean {
1412
- return this.isRunning && this.server !== undefined;
1413
- }
1414
-
1415
- getServerInfo(): { running: boolean } {
1416
- return {
1417
- running: this.isRunning
1418
- };
1419
- }
1420
-
1421
- // Get server information (alias for getServerInfo)
1422
- get info() {
1423
- // Calculate total routes including plugins
1424
- const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
1425
- const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
1426
-
1427
- return {
1428
- // Server status
1429
- running: this.isRunning,
1430
- server: this.server ? 'Bun' : null,
1431
-
1432
- // Connection details
1433
- hostname: this.serverHostname,
1434
- port: this.serverPort,
1435
- url: this.isRunning && this.serverHostname && this.serverPort
1436
- ? `http://${this.serverHostname}:${this.serverPort}`
1437
- : null,
1438
-
1439
- // Application statistics
1440
- totalRoutes,
1441
- totalWsRoutes,
1442
- totalPlugins: this.plugins.length,
1443
-
1444
- // System information
1445
- runtime: 'Bun',
1446
- version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
1447
- pid: process.pid,
1448
- uptime: this.isRunning ? process.uptime() : 0
1449
- };
1450
- }
1451
-
1452
- // Get all routes information
1453
- get routes() {
1454
- // Get routes from main instance
1455
- const mainRoutes = this._routes.map((route: Route) => ({
1456
- method: route.method,
1457
- path: route.path,
1458
- hasConfig: !!route.config,
1459
- config: route.config || null,
1460
- source: 'main' as const
1461
- }));
1462
-
1463
- // Get routes from all plugins
1464
- const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
1465
- plugin._routes.map((route: Route) => ({
1466
- method: route.method,
1467
- path: route.path,
1468
- hasConfig: !!route.config,
1469
- config: route.config || null,
1470
- source: 'plugin' as const,
1471
- pluginIndex
1472
- }))
1473
- );
1474
-
1475
- return [...mainRoutes, ...pluginRoutes];
1476
- }
1477
-
1478
- // Get all WebSocket routes information
1479
- get wsRoutes() {
1480
- // Get WebSocket routes from main instance
1481
- const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
1482
- path: route.path,
1483
- hasHandlers: {
1484
- onOpen: !!route.handler.onOpen,
1485
- onMessage: !!route.handler.onMessage,
1486
- onClose: !!route.handler.onClose,
1487
- onError: !!route.handler.onError
1488
- },
1489
- source: 'main' as const
1490
- }));
1491
-
1492
- // Get WebSocket routes from all plugins
1493
- const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
1494
- plugin._wsRoutes.map((route: WSRoute) => ({
1495
- path: route.path,
1496
- hasHandlers: {
1497
- onOpen: !!route.handler.onOpen,
1498
- onMessage: !!route.handler.onMessage,
1499
- onClose: !!route.handler.onClose,
1500
- onError: !!route.handler.onError
1501
- },
1502
- source: 'plugin' as const,
1503
- pluginIndex
1504
- }))
1505
- );
1506
-
1507
- return [...mainWsRoutes, ...pluginWsRoutes];
1508
- }
1509
- }
1510
-
1511
- const error = (error: Error | string, status: number = 500) => {
1512
- return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
1513
- }
1514
-
1515
- // File helper function (like Elysia)
1516
- const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
1517
- const bunFile = Bun.file(path);
1518
-
1519
- if (options?.type) {
1520
- // Create a wrapper to override the MIME type
1521
- return {
1522
- ...bunFile,
1523
- type: options.type,
1524
- headers: options.headers
1525
- };
1526
- }
1527
-
1528
- return bunFile;
1529
- }
1530
-
1531
- // Redirect helper function (like Elysia)
1532
- const redirect = (location: string, status: number = 302) => {
1533
- return new Response(null, {
1534
- status,
1535
- headers: { Location: location }
1536
- });
1537
- }
1538
-
1539
- // Export Zod for convenience
1540
- export { z, error, file, redirect };
1541
-
1542
- // Export types for external use
1543
- export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, CookieOptions, BXOOptions, Plugin };
1544
-
1545
- // Helper function to create cookie options
1546
- export const createCookieOptions = (
1547
- options: CookieOptions = {}
1548
- ): CookieOptions => ({
1549
- ...options
1550
- });
4
+ // Also export the default BXO class for backward compatibility
5
+ export { default } from './src/index';