bxo 0.0.5-dev.4 → 0.0.5-dev.41

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
@@ -3,6 +3,31 @@ 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
+
6
31
  // OpenAPI detail information
7
32
  interface RouteDetail {
8
33
  summary?: string;
@@ -21,28 +46,54 @@ interface RouteConfig {
21
46
  query?: z.ZodSchema<any>;
22
47
  body?: z.ZodSchema<any>;
23
48
  headers?: z.ZodSchema<any>;
24
- response?: z.ZodSchema<any>;
49
+ cookies?: z.ZodSchema<any>;
50
+ response?: ResponseConfig;
25
51
  detail?: RouteDetail;
26
52
  }
27
53
 
54
+ // Helper type to extract status codes from response config
55
+ type StatusCodes<T> = T extends Record<number, any> ? keyof T : never;
56
+
28
57
  // Context type that's fully typed based on the route configuration
29
58
  export type Context<TConfig extends RouteConfig = {}> = {
30
59
  params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
31
60
  query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
32
61
  body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
33
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;
34
65
  request: Request;
35
66
  set: {
36
67
  status?: number;
37
68
  headers?: Record<string, string>;
69
+ cookies?: Cookie[];
70
+ redirect?: { location: string; status?: number };
38
71
  };
39
- // Extended properties that can be added by plugins
40
- user?: any;
72
+ status: <T extends number>(
73
+ code: TConfig['response'] extends StatusResponseSchema
74
+ ? StatusCodes<TConfig['response']> | number
75
+ : T,
76
+ data?: TConfig['response'] extends StatusResponseSchema
77
+ ? T extends keyof TConfig['response']
78
+ ? InferZodType<TConfig['response'][T]>
79
+ : any
80
+ : TConfig['response'] extends ResponseSchema
81
+ ? InferZodType<TConfig['response']>
82
+ : any
83
+ ) => TConfig['response'] extends StatusResponseSchema
84
+ ? T extends keyof TConfig['response']
85
+ ? InferZodType<TConfig['response'][T]>
86
+ : any
87
+ : TConfig['response'] extends ResponseSchema
88
+ ? InferZodType<TConfig['response']>
89
+ : any;
90
+ redirect: (location: string, status?: number) => Response;
91
+ clearRedirect: () => void;
41
92
  [key: string]: any;
42
93
  };
43
94
 
44
- // Handler function type
45
- type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
95
+ // Handler function type with proper response typing
96
+ type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
46
97
 
47
98
  // Route definition
48
99
  interface Route {
@@ -52,14 +103,39 @@ interface Route {
52
103
  config?: RouteConfig;
53
104
  }
54
105
 
106
+ // WebSocket handler interface
107
+ interface WebSocketHandler {
108
+ onOpen?: (ws: any) => void;
109
+ onMessage?: (ws: any, message: string | Buffer) => void;
110
+ onClose?: (ws: any, code?: number, reason?: string) => void;
111
+ onError?: (ws: any, error: Error) => void;
112
+ }
113
+
114
+ // WebSocket route definition
115
+ interface WSRoute {
116
+ path: string;
117
+ handler: WebSocketHandler;
118
+ }
119
+
55
120
  // Lifecycle hooks
56
121
  interface LifecycleHooks {
57
- onBeforeStart?: () => Promise<void> | void;
58
- onAfterStart?: () => Promise<void> | void;
59
- onBeforeStop?: () => Promise<void> | void;
60
- onAfterStop?: () => Promise<void> | void;
61
- onBeforeRestart?: () => Promise<void> | void;
62
- onAfterRestart?: () => Promise<void> | void;
122
+ onBeforeStart?: (instance: BXO) => Promise<void> | void;
123
+ onAfterStart?: (instance: BXO) => Promise<void> | void;
124
+ onBeforeStop?: (instance: BXO) => Promise<void> | void;
125
+ onAfterStop?: (instance: BXO) => Promise<void> | void;
126
+ onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
127
+ onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
128
+ onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
129
+ }
130
+
131
+ // Add global options for BXO
132
+ interface BXOOptions {
133
+ enableValidation?: boolean;
134
+ }
135
+
136
+ // Plugin interface for middleware-style plugins
137
+ interface Plugin {
138
+ name?: string;
63
139
  onRequest?: (ctx: Context) => Promise<void> | void;
64
140
  onResponse?: (ctx: Context, response: any) => Promise<any> | any;
65
141
  onError?: (ctx: Context, error: Error) => Promise<any> | any;
@@ -67,67 +143,77 @@ interface LifecycleHooks {
67
143
 
68
144
  export default class BXO {
69
145
  private _routes: Route[] = [];
146
+ private _wsRoutes: WSRoute[] = [];
70
147
  private plugins: BXO[] = [];
71
- private hooks: LifecycleHooks = {};
148
+ private middleware: Plugin[] = []; // New middleware array
149
+ private hooks: {
150
+ onBeforeStart?: (instance: BXO) => Promise<void> | void;
151
+ onAfterStart?: (instance: BXO) => Promise<void> | void;
152
+ onBeforeRestart?: (instance: BXO) => Promise<void> | void;
153
+ onAfterRestart?: (instance: BXO) => Promise<void> | void;
154
+ onBeforeStop?: (instance: BXO) => Promise<void> | void;
155
+ onAfterStop?: (instance: BXO) => Promise<void> | void;
156
+ onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
157
+ onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
158
+ onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
159
+ } = {};
72
160
  private server?: any;
73
161
  private isRunning: boolean = false;
74
- private hotReloadEnabled: boolean = false;
75
- private watchedFiles: Set<string> = new Set();
76
- private watchedExclude: Set<string> = new Set();
77
162
  private serverPort?: number;
78
163
  private serverHostname?: string;
164
+ private enableValidation: boolean = true;
79
165
 
80
- constructor() { }
166
+ constructor(options?: BXOOptions) {
167
+ this.enableValidation = options?.enableValidation ?? true;
168
+ }
81
169
 
82
170
  // Lifecycle hook methods
83
- onBeforeStart(handler: () => Promise<void> | void): this {
171
+ onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
84
172
  this.hooks.onBeforeStart = handler;
85
173
  return this;
86
174
  }
87
175
 
88
- onAfterStart(handler: () => Promise<void> | void): this {
176
+ onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
89
177
  this.hooks.onAfterStart = handler;
90
178
  return this;
91
179
  }
92
180
 
93
- onBeforeStop(handler: () => Promise<void> | void): this {
181
+ onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
94
182
  this.hooks.onBeforeStop = handler;
95
183
  return this;
96
184
  }
97
185
 
98
- onAfterStop(handler: () => Promise<void> | void): this {
186
+ onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
99
187
  this.hooks.onAfterStop = handler;
100
188
  return this;
101
189
  }
102
190
 
103
- onBeforeRestart(handler: () => Promise<void> | void): this {
104
- this.hooks.onBeforeRestart = handler;
105
- return this;
106
- }
107
191
 
108
- onAfterRestart(handler: () => Promise<void> | void): this {
109
- this.hooks.onAfterRestart = handler;
110
- return this;
111
- }
112
192
 
113
- onRequest(handler: (ctx: Context) => Promise<void> | void): this {
193
+ onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
114
194
  this.hooks.onRequest = handler;
115
195
  return this;
116
196
  }
117
197
 
118
- onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
198
+ onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
119
199
  this.hooks.onResponse = handler;
120
200
  return this;
121
201
  }
122
202
 
123
- onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
203
+ onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
124
204
  this.hooks.onError = handler;
125
205
  return this;
126
206
  }
127
207
 
128
- // Plugin system - now accepts other BXO instances
129
- use(bxoInstance: BXO): this {
130
- this.plugins.push(bxoInstance);
208
+ // Plugin system - now accepts both BXO instances and middleware plugins
209
+ use(plugin: BXO | Plugin): this {
210
+ if ('_routes' in plugin) {
211
+ // It's a BXO instance
212
+ this.plugins.push(plugin);
213
+ } else {
214
+ // It's a middleware plugin
215
+ this.middleware.push(plugin);
216
+ }
131
217
  return this;
132
218
  }
133
219
 
@@ -222,31 +308,301 @@ export default class BXO {
222
308
  return this;
223
309
  }
224
310
 
311
+ // WebSocket route handler
312
+ ws(path: string, handler: WebSocketHandler): this {
313
+ this._wsRoutes.push({ path, handler });
314
+ return this;
315
+ }
316
+
317
+ // Helper methods to get all routes including plugin routes
318
+ private getAllRoutes(): Route[] {
319
+ const allRoutes = [...this._routes];
320
+ for (const plugin of this.plugins) {
321
+ allRoutes.push(...plugin._routes);
322
+ }
323
+ return allRoutes;
324
+ }
325
+
326
+ private getAllWSRoutes(): WSRoute[] {
327
+ const allWSRoutes = [...this._wsRoutes];
328
+ for (const plugin of this.plugins) {
329
+ allWSRoutes.push(...plugin._wsRoutes);
330
+ }
331
+ return allWSRoutes;
332
+ }
333
+
225
334
  // Route matching utility
226
335
  private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
227
- for (const route of this._routes) {
336
+ const allRoutes = this.getAllRoutes();
337
+
338
+ for (const route of allRoutes) {
228
339
  if (route.method !== method) continue;
229
340
 
230
341
  const routeSegments = route.path.split('/').filter(Boolean);
231
342
  const pathSegments = pathname.split('/').filter(Boolean);
232
343
 
233
- if (routeSegments.length !== pathSegments.length) continue;
344
+ const params: Record<string, string> = {};
345
+ let isMatch = true;
346
+
347
+ // Check for double wildcard (**) in the route
348
+ const hasDoubleWildcard = routeSegments.some(segment => segment === '**');
349
+
350
+ // Handle double wildcard at the end (catch-all with slashes)
351
+ const hasDoubleWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '**';
352
+
353
+ // Handle single wildcard at the end (catch-all)
354
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
355
+
356
+ if (hasDoubleWildcardAtEnd) {
357
+ // For double wildcard at end, path must have at least as many segments as route (minus the double wildcard)
358
+ if (pathSegments.length < routeSegments.length - 1) continue;
359
+ } else if (hasWildcardAtEnd) {
360
+ // For single wildcard at end, path must have at least as many segments as route (minus the wildcard)
361
+ if (pathSegments.length < routeSegments.length - 1) continue;
362
+ } else if (!hasDoubleWildcard) {
363
+ // For exact matching (with possible single-segment wildcards), lengths must match
364
+ if (routeSegments.length !== pathSegments.length) continue;
365
+ }
366
+
367
+ for (let i = 0; i < routeSegments.length; i++) {
368
+ const routeSegment = routeSegments[i];
369
+ const pathSegment = pathSegments[i];
370
+
371
+ if (!routeSegment) {
372
+ isMatch = false;
373
+ break;
374
+ }
375
+
376
+ // Handle double wildcard at the end (matches everything including slashes)
377
+ if (routeSegment === '**' && i === routeSegments.length - 1) {
378
+ const remainingPath = pathSegments.slice(i).join('/');
379
+ params['**'] = remainingPath;
380
+ break;
381
+ }
382
+
383
+ // Handle single wildcard at the end (catch-all)
384
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
385
+ // Wildcard at end matches remaining path segments
386
+ const remainingPath = pathSegments.slice(i).join('/');
387
+ params['*'] = remainingPath;
388
+ break;
389
+ }
390
+
391
+ // Handle double wildcard in the middle (matches everything including slashes)
392
+ if (routeSegment === '**') {
393
+ // Find the next non-wildcard segment to match against
394
+ let nextNonWildcardIndex = i + 1;
395
+ while (nextNonWildcardIndex < routeSegments.length &&
396
+ (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
397
+ nextNonWildcardIndex++;
398
+ }
399
+
400
+ if (nextNonWildcardIndex >= routeSegments.length) {
401
+ // Double wildcard is at the end or followed by other wildcards
402
+ const remainingPath = pathSegments.slice(i).join('/');
403
+ params['**'] = remainingPath;
404
+ break;
405
+ }
406
+
407
+ // Find the next matching segment in the path
408
+ const nextRouteSegment = routeSegments[nextNonWildcardIndex];
409
+ if (!nextRouteSegment) {
410
+ isMatch = false;
411
+ break;
412
+ }
413
+
414
+ let foundMatch = false;
415
+ let matchedPath = '';
416
+
417
+ for (let j = i; j < pathSegments.length; j++) {
418
+ const currentPathSegment = pathSegments[j];
419
+
420
+ // Check if this path segment matches the next route segment
421
+ if (nextRouteSegment.startsWith(':')) {
422
+ // Param segment - always matches
423
+ matchedPath = pathSegments.slice(i, j).join('/');
424
+ params['**'] = matchedPath;
425
+ i = j - 1; // Adjust index for the next iteration
426
+ foundMatch = true;
427
+ break;
428
+ } else if (nextRouteSegment === '*') {
429
+ // Single wildcard - always matches
430
+ matchedPath = pathSegments.slice(i, j).join('/');
431
+ params['**'] = matchedPath;
432
+ i = j - 1; // Adjust index for the next iteration
433
+ foundMatch = true;
434
+ break;
435
+ } else if (nextRouteSegment === currentPathSegment) {
436
+ // Exact match
437
+ matchedPath = pathSegments.slice(i, j).join('/');
438
+ params['**'] = matchedPath;
439
+ i = j - 1; // Adjust index for the next iteration
440
+ foundMatch = true;
441
+ break;
442
+ }
443
+ }
444
+
445
+ if (!foundMatch) {
446
+ isMatch = false;
447
+ break;
448
+ }
449
+
450
+ continue;
451
+ }
452
+
453
+ if (!pathSegment) {
454
+ isMatch = false;
455
+ break;
456
+ }
457
+
458
+ if (routeSegment.startsWith(':')) {
459
+ const paramName = routeSegment.slice(1);
460
+ params[paramName] = pathSegment;
461
+ } else if (routeSegment === '*') {
462
+ // Single segment wildcard
463
+ params['*'] = pathSegment;
464
+ } else if (routeSegment !== pathSegment) {
465
+ isMatch = false;
466
+ break;
467
+ }
468
+ }
469
+
470
+ if (isMatch) {
471
+ return { route, params };
472
+ }
473
+ }
474
+
475
+ return null;
476
+ }
477
+
478
+ // WebSocket route matching utility
479
+ private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
480
+ const allWSRoutes = this.getAllWSRoutes();
481
+
482
+ for (const route of allWSRoutes) {
483
+ const routeSegments = route.path.split('/').filter(Boolean);
484
+ const pathSegments = pathname.split('/').filter(Boolean);
234
485
 
235
486
  const params: Record<string, string> = {};
236
487
  let isMatch = true;
237
488
 
489
+ // Check for double wildcard (**) in the route
490
+ const hasDoubleWildcard = routeSegments.some(segment => segment === '**');
491
+
492
+ // Handle double wildcard at the end (catch-all with slashes)
493
+ const hasDoubleWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '**';
494
+
495
+ // Handle single wildcard at the end (catch-all)
496
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
497
+
498
+ if (hasDoubleWildcardAtEnd) {
499
+ // For double wildcard at end, path must have at least as many segments as route (minus the double wildcard)
500
+ if (pathSegments.length < routeSegments.length - 1) continue;
501
+ } else if (hasWildcardAtEnd) {
502
+ // For single wildcard at end, path must have at least as many segments as route (minus the wildcard)
503
+ if (pathSegments.length < routeSegments.length - 1) continue;
504
+ } else if (!hasDoubleWildcard) {
505
+ // For exact matching (with possible single-segment wildcards), lengths must match
506
+ if (routeSegments.length !== pathSegments.length) continue;
507
+ }
508
+
238
509
  for (let i = 0; i < routeSegments.length; i++) {
239
510
  const routeSegment = routeSegments[i];
240
511
  const pathSegment = pathSegments[i];
241
512
 
242
- if (!routeSegment || !pathSegment) {
513
+ if (!routeSegment) {
514
+ isMatch = false;
515
+ break;
516
+ }
517
+
518
+ // Handle double wildcard at the end (matches everything including slashes)
519
+ if (routeSegment === '**' && i === routeSegments.length - 1) {
520
+ const remainingPath = pathSegments.slice(i).join('/');
521
+ params['**'] = remainingPath;
522
+ break;
523
+ }
524
+
525
+ // Handle single wildcard at the end (catch-all)
526
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
527
+ // Wildcard at end matches remaining path segments
528
+ const remainingPath = pathSegments.slice(i).join('/');
529
+ params['*'] = remainingPath;
530
+ break;
531
+ }
532
+
533
+ // Handle double wildcard in the middle (matches everything including slashes)
534
+ if (routeSegment === '**') {
535
+ // Find the next non-wildcard segment to match against
536
+ let nextNonWildcardIndex = i + 1;
537
+ while (nextNonWildcardIndex < routeSegments.length &&
538
+ (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
539
+ nextNonWildcardIndex++;
540
+ }
541
+
542
+ if (nextNonWildcardIndex >= routeSegments.length) {
543
+ // Double wildcard is at the end or followed by other wildcards
544
+ const remainingPath = pathSegments.slice(i).join('/');
545
+ params['**'] = remainingPath;
546
+ break;
547
+ }
548
+
549
+ // Find the next matching segment in the path
550
+ const nextRouteSegment = routeSegments[nextNonWildcardIndex];
551
+ if (!nextRouteSegment) {
552
+ isMatch = false;
553
+ break;
554
+ }
555
+
556
+ let foundMatch = false;
557
+ let matchedPath = '';
558
+
559
+ for (let j = i; j < pathSegments.length; j++) {
560
+ const currentPathSegment = pathSegments[j];
561
+
562
+ // Check if this path segment matches the next route segment
563
+ if (nextRouteSegment.startsWith(':')) {
564
+ // Param segment - always matches
565
+ matchedPath = pathSegments.slice(i, j).join('/');
566
+ params['**'] = matchedPath;
567
+ i = j - 1; // Adjust index for the next iteration
568
+ foundMatch = true;
569
+ break;
570
+ } else if (nextRouteSegment === '*') {
571
+ // Single wildcard - always matches
572
+ matchedPath = pathSegments.slice(i, j).join('/');
573
+ params['**'] = matchedPath;
574
+ i = j - 1; // Adjust index for the next iteration
575
+ foundMatch = true;
576
+ break;
577
+ } else if (nextRouteSegment === currentPathSegment) {
578
+ // Exact match
579
+ matchedPath = pathSegments.slice(i, j).join('/');
580
+ params['**'] = matchedPath;
581
+ i = j - 1; // Adjust index for the next iteration
582
+ foundMatch = true;
583
+ break;
584
+ }
585
+ }
586
+
587
+ if (!foundMatch) {
588
+ isMatch = false;
589
+ break;
590
+ }
591
+
592
+ continue;
593
+ }
594
+
595
+ if (!pathSegment) {
243
596
  isMatch = false;
244
597
  break;
245
598
  }
246
599
 
247
600
  if (routeSegment.startsWith(':')) {
248
601
  const paramName = routeSegment.slice(1);
249
- params[paramName] = decodeURIComponent(pathSegment);
602
+ params[paramName] = pathSegment;
603
+ } else if (routeSegment === '*') {
604
+ // Single segment wildcard
605
+ params['*'] = pathSegment;
250
606
  } else if (routeSegment !== pathSegment) {
251
607
  isMatch = false;
252
608
  break;
@@ -264,32 +620,106 @@ export default class BXO {
264
620
  // Parse query string
265
621
  private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
266
622
  const query: Record<string, string | undefined> = {};
267
- for (const [key, value] of searchParams.entries()) {
623
+ searchParams.forEach((value, key) => {
268
624
  query[key] = value;
269
- }
625
+ });
270
626
  return query;
271
627
  }
272
628
 
273
629
  // Parse headers
274
630
  private parseHeaders(headers: Headers): Record<string, string> {
275
631
  const headerObj: Record<string, string> = {};
276
- for (const [key, value] of headers.entries()) {
632
+ headers.forEach((value, key) => {
277
633
  headerObj[key] = value;
278
- }
634
+ });
279
635
  return headerObj;
280
636
  }
281
637
 
638
+ // Parse cookies from Cookie header
639
+ private parseCookies(cookieHeader: string | null): Record<string, string> {
640
+ const cookies: Record<string, string> = {};
641
+
642
+ if (!cookieHeader) return cookies;
643
+
644
+ const cookiePairs = cookieHeader.split(';');
645
+ for (const pair of cookiePairs) {
646
+ const [name, value] = pair.trim().split('=');
647
+ if (name && value) {
648
+ cookies[decodeURIComponent(name)] = decodeURIComponent(value);
649
+ }
650
+ }
651
+
652
+ return cookies;
653
+ }
654
+
282
655
  // Validate data against Zod schema
283
656
  private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
284
657
  if (!schema) return data;
285
658
  return schema.parse(data);
286
659
  }
287
660
 
661
+ // Validate response against response config (supports both simple and status-based schemas)
662
+ private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
663
+ if (!responseConfig || !this.enableValidation) return data;
664
+
665
+ // If it's a simple schema (not status-based)
666
+ if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
667
+ return responseConfig.parse(data);
668
+ }
669
+
670
+ // If it's a status-based schema
671
+ if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
672
+ const statusSchema = responseConfig[status];
673
+ if (statusSchema) {
674
+ return statusSchema.parse(data);
675
+ }
676
+
677
+ // If no specific status schema found, try to find a fallback
678
+ // Common fallback statuses: 200, 201, 400, 500
679
+ const fallbackStatuses = [200, 201, 400, 500];
680
+ for (const fallbackStatus of fallbackStatuses) {
681
+ if (responseConfig[fallbackStatus]) {
682
+ return responseConfig[fallbackStatus]?.parse(data);
683
+ }
684
+ }
685
+
686
+ // If no schema found for the status, return data as-is
687
+ return data;
688
+ }
689
+
690
+ return data;
691
+ }
692
+
288
693
  // Main request handler
289
- private async handleRequest(request: Request): Promise<Response> {
694
+ private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
290
695
  const url = new URL(request.url);
291
696
  const method = request.method;
292
- const pathname = url.pathname;
697
+ const rawPathname = url.pathname;
698
+ let pathname: string;
699
+ try {
700
+ pathname = decodeURI(rawPathname);
701
+ } catch {
702
+ pathname = rawPathname;
703
+ }
704
+
705
+ // Check for WebSocket upgrade
706
+ if (request.headers.get('upgrade') === 'websocket') {
707
+ const wsMatchResult = this.matchWSRoute(pathname);
708
+ if (wsMatchResult && server) {
709
+ const success = server.upgrade(request, {
710
+ data: {
711
+ handler: wsMatchResult.route.handler,
712
+ params: wsMatchResult.params,
713
+ pathname
714
+ }
715
+ });
716
+
717
+ if (success) {
718
+ return; // undefined response means upgrade was successful
719
+ }
720
+ }
721
+ return new Response('WebSocket upgrade failed', { status: 400 });
722
+ }
293
723
 
294
724
  const matchResult = this.matchRoute(method, pathname);
295
725
  if (!matchResult) {
@@ -299,6 +729,7 @@ export default class BXO {
299
729
  const { route, params } = matchResult;
300
730
  const query = this.parseQuery(url.searchParams);
301
731
  const headers = this.parseHeaders(request.headers);
732
+ const cookies = this.parseCookies(request.headers.get('cookie'));
302
733
 
303
734
  let body: any;
304
735
  if (request.method !== 'GET' && request.method !== 'HEAD') {
@@ -313,56 +744,231 @@ export default class BXO {
313
744
  const formData = await request.formData();
314
745
  body = Object.fromEntries(formData.entries());
315
746
  } else {
316
- body = await request.text();
747
+ // Try to parse as JSON if it looks like JSON, otherwise treat as text
748
+ const textBody = await request.text();
749
+ try {
750
+ // Check if the text looks like JSON
751
+ if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
752
+ body = JSON.parse(textBody);
753
+ } else {
754
+ body = textBody;
755
+ }
756
+ } catch {
757
+ body = textBody;
758
+ }
317
759
  }
318
760
  }
319
761
 
320
- // Create context
321
- const ctx: Context = {
322
- params: route.config?.params ? this.validateData(route.config.params, params) : params,
323
- query: route.config?.query ? this.validateData(route.config.query, query) : query,
324
- body: route.config?.body ? this.validateData(route.config.body, body) : body,
325
- headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
326
- request,
327
- set: {}
328
- };
762
+ // Create context with validation
763
+ let ctx: Context;
764
+ try {
765
+ // Validate each part separately to get better error messages
766
+ const validatedParams = this.enableValidation && route.config?.params ? this.validateData(route.config.params, params) : params;
767
+ const validatedQuery = this.enableValidation && route.config?.query ? this.validateData(route.config.query, query) : query;
768
+ const validatedBody = this.enableValidation && route.config?.body ? this.validateData(route.config.body, body) : body;
769
+ const validatedHeaders = this.enableValidation && route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
770
+ const validatedCookies = this.enableValidation && route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
771
+
772
+ ctx = {
773
+ params: validatedParams,
774
+ query: validatedQuery,
775
+ body: validatedBody,
776
+ headers: validatedHeaders,
777
+ cookies: validatedCookies,
778
+ path: pathname,
779
+ request,
780
+ set: {},
781
+ status: ((code: number, data?: any) => {
782
+ ctx.set.status = code;
783
+ return data;
784
+ }) as any,
785
+ redirect: ((location: string, status: number = 302) => {
786
+ // Record redirect intent only; avoid mutating generic status/headers so it can be canceled later
787
+ ctx.set.redirect = { location, status };
788
+
789
+ // Prepare headers for immediate Response return without persisting to ctx.set.headers
790
+ const responseHeaders: Record<string, string> = {
791
+ Location: location,
792
+ ...(ctx.set.headers || {})
793
+ };
794
+
795
+ // Handle cookies if any are set on context
796
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
797
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
798
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
799
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
800
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
801
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
802
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
803
+ if (cookie.secure) cookieString += `; Secure`;
804
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
805
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
806
+ return cookieString;
807
+ });
808
+ cookieHeaders.forEach((cookieHeader, index) => {
809
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
810
+ });
811
+ }
812
+
813
+ return new Response(null, {
814
+ status,
815
+ headers: responseHeaders
816
+ });
817
+ }) as any,
818
+ clearRedirect: (() => {
819
+ // Clear explicit redirect intent
820
+ delete ctx.set.redirect;
821
+ // Remove any Location header if present
822
+ if (ctx.set.headers) {
823
+ for (const key of Object.keys(ctx.set.headers)) {
824
+ if (key.toLowerCase() === 'location') {
825
+ delete ctx.set.headers[key];
826
+ }
827
+ }
828
+ }
829
+ // Reset status if it is a redirect
830
+ if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
831
+ delete ctx.set.status;
832
+ }
833
+ }) as any
834
+ };
835
+ } catch (validationError) {
836
+ // Validation failed - return error response
837
+
838
+ // Extract detailed validation errors from Zod
839
+ let validationDetails = undefined;
840
+ if (validationError instanceof Error) {
841
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
842
+ validationDetails = validationError.errors;
843
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
844
+ validationDetails = validationError.issues;
845
+ }
846
+ }
847
+
848
+ // Create a clean error message
849
+ const errorMessage = validationDetails && validationDetails.length > 0
850
+ ? `Validation failed for ${validationDetails.length} field(s)`
851
+ : 'Validation failed';
852
+
853
+ return new Response(JSON.stringify({
854
+ error: errorMessage,
855
+ details: validationDetails
856
+ }), {
857
+ status: 400,
858
+ headers: { 'Content-Type': 'application/json' }
859
+ });
860
+ }
329
861
 
330
862
  try {
863
+ // Run middleware onRequest hooks
864
+ for (const plugin of this.middleware) {
865
+ if (plugin.onRequest) {
866
+ await plugin.onRequest(ctx);
867
+ }
868
+ }
869
+
331
870
  // Run global onRequest hook
332
871
  if (this.hooks.onRequest) {
333
- await this.hooks.onRequest(ctx);
872
+ await this.hooks.onRequest(ctx, this);
334
873
  }
335
874
 
336
875
  // Run BXO instance onRequest hooks
337
876
  for (const bxoInstance of this.plugins) {
338
877
  if (bxoInstance.hooks.onRequest) {
339
- await bxoInstance.hooks.onRequest(ctx);
878
+ await bxoInstance.hooks.onRequest(ctx, this);
340
879
  }
341
880
  }
342
881
 
343
882
  // Execute route handler
344
883
  let response = await route.handler(ctx);
345
884
 
885
+ // Run middleware onResponse hooks
886
+ for (const plugin of this.middleware) {
887
+ if (plugin.onResponse) {
888
+ response = await plugin.onResponse(ctx, response) || response;
889
+ }
890
+ }
891
+
346
892
  // Run global onResponse hook
347
893
  if (this.hooks.onResponse) {
348
- response = await this.hooks.onResponse(ctx, response) || response;
894
+ response = await this.hooks.onResponse(ctx, response, this) || response;
349
895
  }
350
896
 
351
897
  // Run BXO instance onResponse hooks
352
898
  for (const bxoInstance of this.plugins) {
353
899
  if (bxoInstance.hooks.onResponse) {
354
- response = await bxoInstance.hooks.onResponse(ctx, response) || response;
900
+ response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
901
+ }
902
+ }
903
+
904
+ // If the handler did not return a response, but a redirect was configured via ctx.set,
905
+ // automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
906
+ const hasImplicitRedirectIntent = !!ctx.set.redirect
907
+ || (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400);
908
+ if ((response === undefined || response === null) && hasImplicitRedirectIntent) {
909
+ const locationFromHeaders = ctx.set.headers && Object.entries(ctx.set.headers).find(([k]) => k.toLowerCase() === 'location')?.[1];
910
+ const location = ctx.set.redirect?.location || locationFromHeaders;
911
+ if (location) {
912
+ // Build headers, ensuring Location is present
913
+ let responseHeaders: Record<string, string> = ctx.set.headers ? { ...ctx.set.headers } : {};
914
+ if (!Object.keys(responseHeaders).some(k => k.toLowerCase() === 'location')) {
915
+ responseHeaders['Location'] = location;
916
+ }
917
+ // Determine status precedence: redirect.status > set.status > 302
918
+ const status = ctx.set.redirect?.status ?? ctx.set.status ?? 302;
919
+
920
+ // Handle cookies if any are set
921
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
922
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
923
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
924
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
925
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
926
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
927
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
928
+ if (cookie.secure) cookieString += `; Secure`;
929
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
930
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
931
+ return cookieString;
932
+ });
933
+ cookieHeaders.forEach((cookieHeader, index) => {
934
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
935
+ });
936
+ }
937
+
938
+ return new Response(null, {
939
+ status,
940
+ headers: responseHeaders
941
+ });
355
942
  }
356
943
  }
357
944
 
358
945
  // Validate response against schema if provided
359
- if (route.config?.response && !(response instanceof Response)) {
946
+ if (this.enableValidation && route.config?.response && !(response instanceof Response)) {
360
947
  try {
361
- response = this.validateData(route.config.response, response);
948
+ const status = ctx.set.status || 200;
949
+ response = this.validateResponse(route.config.response, response, status);
362
950
  } catch (validationError) {
363
951
  // Response validation failed
364
- const errorMessage = validationError instanceof Error ? validationError.message : 'Response validation failed';
365
- return new Response(JSON.stringify({ error: `Response validation error: ${errorMessage}` }), {
952
+
953
+ // Extract detailed validation errors from Zod
954
+ let validationDetails = undefined;
955
+ if (validationError instanceof Error) {
956
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
957
+ validationDetails = validationError.errors;
958
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
959
+ validationDetails = validationError.issues;
960
+ }
961
+ }
962
+
963
+ // Create a clean error message
964
+ const errorMessage = validationDetails && validationDetails.length > 0
965
+ ? `Response validation failed for ${validationDetails.length} field(s)`
966
+ : 'Response validation failed';
967
+
968
+ return new Response(JSON.stringify({
969
+ error: errorMessage,
970
+ details: validationDetails
971
+ }), {
366
972
  status: 500,
367
973
  headers: { 'Content-Type': 'application/json' }
368
974
  });
@@ -374,9 +980,63 @@ export default class BXO {
374
980
  return response;
375
981
  }
376
982
 
983
+ // Handle File response (like Elysia)
984
+ if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
985
+ const file = response as File;
986
+ const responseInit: ResponseInit = {
987
+ status: ctx.set.status || 200,
988
+ headers: {
989
+ 'Content-Type': file.type || 'application/octet-stream',
990
+ 'Content-Length': file.size.toString(),
991
+ ...ctx.set.headers
992
+ }
993
+ };
994
+ return new Response(file, responseInit);
995
+ }
996
+
997
+ // Handle Bun.file() response
998
+ if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
999
+ const bunFile = response as any;
1000
+ const responseInit: ResponseInit = {
1001
+ status: ctx.set.status || 200,
1002
+ headers: {
1003
+ 'Content-Type': bunFile.type || 'application/octet-stream',
1004
+ 'Content-Length': bunFile.size?.toString() || '',
1005
+ ...ctx.set.headers,
1006
+ ...(bunFile.headers || {}) // Support custom headers from file helper
1007
+ }
1008
+ };
1009
+ return new Response(bunFile, responseInit);
1010
+ }
1011
+
1012
+ // Prepare headers with cookies
1013
+ let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
1014
+
1015
+ // Handle cookies if any are set
1016
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
1017
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
1018
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
1019
+
1020
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
1021
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
1022
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
1023
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
1024
+ if (cookie.secure) cookieString += `; Secure`;
1025
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
1026
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
1027
+
1028
+ return cookieString;
1029
+ });
1030
+
1031
+ // Add Set-Cookie headers
1032
+ cookieHeaders.forEach((cookieHeader, index) => {
1033
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
1034
+ });
1035
+ }
1036
+
377
1037
  const responseInit: ResponseInit = {
378
1038
  status: ctx.set.status || 200,
379
- headers: ctx.set.headers || {}
1039
+ headers: responseHeaders
380
1040
  };
381
1041
 
382
1042
  if (typeof response === 'string') {
@@ -395,13 +1055,20 @@ export default class BXO {
395
1055
  // Run error hooks
396
1056
  let errorResponse: any;
397
1057
 
1058
+ // Run middleware onError hooks
1059
+ for (const plugin of this.middleware) {
1060
+ if (plugin.onError) {
1061
+ errorResponse = await plugin.onError(ctx, error as Error) || errorResponse;
1062
+ }
1063
+ }
1064
+
398
1065
  if (this.hooks.onError) {
399
- errorResponse = await this.hooks.onError(ctx, error as Error);
1066
+ errorResponse = await this.hooks.onError(ctx, error as Error, this);
400
1067
  }
401
1068
 
402
1069
  for (const bxoInstance of this.plugins) {
403
1070
  if (bxoInstance.hooks.onError) {
404
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
1071
+ errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
405
1072
  }
406
1073
  }
407
1074
 
@@ -424,77 +1091,7 @@ export default class BXO {
424
1091
  }
425
1092
  }
426
1093
 
427
- // Hot reload functionality
428
- enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
429
- this.hotReloadEnabled = true;
430
- watchPaths.forEach(path => this.watchedFiles.add(path));
431
- excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
432
- return this;
433
- }
434
1094
 
435
- private shouldExcludeFile(filename: string): boolean {
436
- for (const pattern of this.watchedExclude) {
437
- // Handle exact match
438
- if (pattern === filename) {
439
- return true;
440
- }
441
-
442
- // Handle directory patterns (e.g., "node_modules/", "dist/")
443
- if (pattern.endsWith('/')) {
444
- if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
445
- return true;
446
- }
447
- }
448
-
449
- // Handle wildcard patterns (e.g., "*.log", "temp*")
450
- if (pattern.includes('*')) {
451
- const regex = new RegExp(pattern.replace(/\*/g, '.*'));
452
- if (regex.test(filename)) {
453
- return true;
454
- }
455
- }
456
-
457
- // Handle file extension patterns (e.g., ".log", ".tmp")
458
- if (pattern.startsWith('.') && filename.endsWith(pattern)) {
459
- return true;
460
- }
461
-
462
- // Handle substring matches for directories
463
- if (filename.includes(pattern)) {
464
- return true;
465
- }
466
- }
467
-
468
- return false;
469
- }
470
-
471
- private async setupFileWatcher(port: number, hostname: string): Promise<void> {
472
- if (!this.hotReloadEnabled) return;
473
-
474
- const fs = require('fs');
475
-
476
- for (const watchPath of this.watchedFiles) {
477
- try {
478
- fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
479
- if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
480
- // Check if file should be excluded
481
- if (this.shouldExcludeFile(filename)) {
482
- return;
483
- }
484
-
485
- console.log(`🔄 File changed: ${filename}, restarting server...`);
486
- await this.restart(port, hostname);
487
- }
488
- });
489
- console.log(`👀 Watching ${watchPath} for changes...`);
490
- if (this.watchedExclude.size > 0) {
491
- console.log(`🚫 Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
492
- }
493
- } catch (error) {
494
- console.warn(`⚠️ Could not watch ${watchPath}:`, error);
495
- }
496
- }
497
- }
498
1095
 
499
1096
  // Server management methods
500
1097
  async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
@@ -506,29 +1103,49 @@ export default class BXO {
506
1103
  try {
507
1104
  // Before start hook
508
1105
  if (this.hooks.onBeforeStart) {
509
- await this.hooks.onBeforeStart();
1106
+ await this.hooks.onBeforeStart(this);
510
1107
  }
511
1108
 
512
1109
  this.server = Bun.serve({
513
1110
  port,
514
1111
  hostname,
515
- fetch: (request) => this.handleRequest(request),
1112
+ fetch: (request, server) => this.handleRequest(request, server),
1113
+ websocket: {
1114
+ message: (ws: any, message: any) => {
1115
+ const handler = ws.data?.handler;
1116
+ if (handler?.onMessage) {
1117
+ handler.onMessage(ws, message);
1118
+ }
1119
+ },
1120
+ open: (ws: any) => {
1121
+ const handler = ws.data?.handler;
1122
+ if (handler?.onOpen) {
1123
+ handler.onOpen(ws);
1124
+ }
1125
+ },
1126
+ close: (ws: any, code?: number, reason?: string) => {
1127
+ const handler = ws.data?.handler;
1128
+ if (handler?.onClose) {
1129
+ handler.onClose(ws, code, reason);
1130
+ }
1131
+ }
1132
+ }
516
1133
  });
517
1134
 
1135
+ // Verify server was created successfully
1136
+ if (!this.server) {
1137
+ throw new Error('Failed to create server instance');
1138
+ }
1139
+
518
1140
  this.isRunning = true;
519
1141
  this.serverPort = port;
520
1142
  this.serverHostname = hostname;
521
1143
 
522
- console.log(`🦊 BXO server running at http://${hostname}:${port}`);
523
-
524
1144
  // After start hook
525
1145
  if (this.hooks.onAfterStart) {
526
- await this.hooks.onAfterStart();
1146
+ await this.hooks.onAfterStart(this);
527
1147
  }
528
1148
 
529
- // Setup hot reload
530
- await this.setupFileWatcher(port, hostname);
531
-
532
1149
  // Handle graceful shutdown
533
1150
  const shutdownHandler = async () => {
534
1151
  await this.stop();
@@ -553,57 +1170,49 @@ export default class BXO {
553
1170
  try {
554
1171
  // Before stop hook
555
1172
  if (this.hooks.onBeforeStop) {
556
- await this.hooks.onBeforeStop();
1173
+ await this.hooks.onBeforeStop(this);
557
1174
  }
558
1175
 
559
1176
  if (this.server) {
560
- this.server.stop();
561
- this.server = null;
1177
+ try {
1178
+ // Try to stop the server gracefully
1179
+ if (typeof this.server.stop === 'function') {
1180
+ this.server.stop();
1181
+ } else {
1182
+ console.warn('⚠️ Server stop method not available');
1183
+ }
1184
+ } catch (stopError) {
1185
+ console.error('❌ Error calling server.stop():', stopError);
1186
+ }
1187
+
1188
+ // Clear the server reference
1189
+ this.server = undefined;
562
1190
  }
563
1191
 
1192
+ // Reset state regardless of server.stop() success
564
1193
  this.isRunning = false;
565
1194
  this.serverPort = undefined;
566
1195
  this.serverHostname = undefined;
567
1196
 
568
- console.log('🛑 BXO server stopped');
569
-
570
1197
  // After stop hook
571
1198
  if (this.hooks.onAfterStop) {
572
- await this.hooks.onAfterStop();
1199
+ await this.hooks.onAfterStop(this);
573
1200
  }
574
1201
 
1202
+ console.log('✅ Server stopped successfully');
1203
+
575
1204
  } catch (error) {
576
1205
  console.error('❌ Error stopping server:', error);
1206
+ // Even if there's an error, reset the state
1207
+ this.isRunning = false;
1208
+ this.server = undefined;
1209
+ this.serverPort = undefined;
1210
+ this.serverHostname = undefined;
577
1211
  throw error;
578
1212
  }
579
1213
  }
580
1214
 
581
- async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
582
- try {
583
- // Before restart hook
584
- if (this.hooks.onBeforeRestart) {
585
- await this.hooks.onBeforeRestart();
586
- }
587
-
588
- console.log('🔄 Restarting BXO server...');
589
-
590
- await this.stop();
591
1215
 
592
- // Small delay to ensure cleanup
593
- await new Promise(resolve => setTimeout(resolve, 100));
594
-
595
- await this.start(port, hostname);
596
-
597
- // After restart hook
598
- if (this.hooks.onAfterRestart) {
599
- await this.hooks.onAfterRestart();
600
- }
601
-
602
- } catch (error) {
603
- console.error('❌ Error restarting server:', error);
604
- throw error;
605
- }
606
- }
607
1216
 
608
1217
  // Backward compatibility
609
1218
  async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
@@ -612,41 +1221,38 @@ export default class BXO {
612
1221
 
613
1222
  // Server status
614
1223
  isServerRunning(): boolean {
615
- return this.isRunning;
1224
+ return this.isRunning && this.server !== undefined;
616
1225
  }
617
1226
 
618
- getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
1227
+ getServerInfo(): { running: boolean } {
619
1228
  return {
620
- running: this.isRunning,
621
- hotReload: this.hotReloadEnabled,
622
- watchedFiles: Array.from(this.watchedFiles),
623
- excludePatterns: Array.from(this.watchedExclude)
1229
+ running: this.isRunning
624
1230
  };
625
1231
  }
626
1232
 
627
1233
  // Get server information (alias for getServerInfo)
628
1234
  get info() {
1235
+ // Calculate total routes including plugins
1236
+ const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
1237
+ const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
1238
+
629
1239
  return {
630
1240
  // Server status
631
1241
  running: this.isRunning,
632
1242
  server: this.server ? 'Bun' : null,
633
-
1243
+
634
1244
  // Connection details
635
1245
  hostname: this.serverHostname,
636
1246
  port: this.serverPort,
637
- url: this.isRunning && this.serverHostname && this.serverPort
638
- ? `http://${this.serverHostname}:${this.serverPort}`
1247
+ url: this.isRunning && this.serverHostname && this.serverPort
1248
+ ? `http://${this.serverHostname}:${this.serverPort}`
639
1249
  : null,
640
-
1250
+
641
1251
  // Application statistics
642
- totalRoutes: this._routes.length,
1252
+ totalRoutes,
1253
+ totalWsRoutes,
643
1254
  totalPlugins: this.plugins.length,
644
-
645
- // Hot reload configuration
646
- hotReload: this.hotReloadEnabled,
647
- watchedFiles: Array.from(this.watchedFiles),
648
- excludePatterns: Array.from(this.watchedExclude),
649
-
1255
+
650
1256
  // System information
651
1257
  runtime: 'Bun',
652
1258
  version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
@@ -657,21 +1263,104 @@ export default class BXO {
657
1263
 
658
1264
  // Get all routes information
659
1265
  get routes() {
660
- return this._routes.map((route: Route) => ({
1266
+ // Get routes from main instance
1267
+ const mainRoutes = this._routes.map((route: Route) => ({
661
1268
  method: route.method,
662
1269
  path: route.path,
663
1270
  hasConfig: !!route.config,
664
- config: route.config || null
1271
+ config: route.config || null,
1272
+ source: 'main' as const
1273
+ }));
1274
+
1275
+ // Get routes from all plugins
1276
+ const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
1277
+ plugin._routes.map((route: Route) => ({
1278
+ method: route.method,
1279
+ path: route.path,
1280
+ hasConfig: !!route.config,
1281
+ config: route.config || null,
1282
+ source: 'plugin' as const,
1283
+ pluginIndex
1284
+ }))
1285
+ );
1286
+
1287
+ return [...mainRoutes, ...pluginRoutes];
1288
+ }
1289
+
1290
+ // Get all WebSocket routes information
1291
+ get wsRoutes() {
1292
+ // Get WebSocket routes from main instance
1293
+ const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
1294
+ path: route.path,
1295
+ hasHandlers: {
1296
+ onOpen: !!route.handler.onOpen,
1297
+ onMessage: !!route.handler.onMessage,
1298
+ onClose: !!route.handler.onClose,
1299
+ onError: !!route.handler.onError
1300
+ },
1301
+ source: 'main' as const
665
1302
  }));
1303
+
1304
+ // Get WebSocket routes from all plugins
1305
+ const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
1306
+ plugin._wsRoutes.map((route: WSRoute) => ({
1307
+ path: route.path,
1308
+ hasHandlers: {
1309
+ onOpen: !!route.handler.onOpen,
1310
+ onMessage: !!route.handler.onMessage,
1311
+ onClose: !!route.handler.onClose,
1312
+ onError: !!route.handler.onError
1313
+ },
1314
+ source: 'plugin' as const,
1315
+ pluginIndex
1316
+ }))
1317
+ );
1318
+
1319
+ return [...mainWsRoutes, ...pluginWsRoutes];
1320
+ }
1321
+ }
1322
+
1323
+ const error = (error: Error | string, status: number = 500) => {
1324
+ return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
1325
+ }
1326
+
1327
+ // File helper function (like Elysia)
1328
+ const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
1329
+ const bunFile = Bun.file(path);
1330
+
1331
+ if (options?.type) {
1332
+ // Create a wrapper to override the MIME type
1333
+ return {
1334
+ ...bunFile,
1335
+ type: options.type,
1336
+ headers: options.headers
1337
+ };
666
1338
  }
1339
+
1340
+ return bunFile;
667
1341
  }
668
1342
 
669
- const error = (error: Error, status: number = 500) => {
670
- return new Response(JSON.stringify({ error: error.message }), { status });
1343
+ // Redirect helper function (like Elysia)
1344
+ const redirect = (location: string, status: number = 302) => {
1345
+ return new Response(null, {
1346
+ status,
1347
+ headers: { Location: location }
1348
+ });
671
1349
  }
672
1350
 
673
1351
  // Export Zod for convenience
674
- export { z, error };
1352
+ export { z, error, file, redirect };
675
1353
 
676
1354
  // Export types for external use
677
- export type { RouteConfig, RouteDetail, Handler };
1355
+ export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions };
1356
+
1357
+ // Helper function to create a cookie
1358
+ export const createCookie = (
1359
+ name: string,
1360
+ value: string,
1361
+ options: Omit<Cookie, 'name' | 'value'> = {}
1362
+ ): Cookie => ({
1363
+ name,
1364
+ value,
1365
+ ...options
1366
+ });