bxo 0.0.5-dev.2 → 0.0.5-dev.21

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