bxo 0.0.5-dev.1 → 0.0.5-dev.10

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.
Files changed (2) hide show
  1. package/index.ts +319 -51
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -3,6 +3,18 @@ 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
+ // OpenAPI detail information
7
+ interface RouteDetail {
8
+ summary?: string;
9
+ description?: string;
10
+ tags?: string[];
11
+ operationId?: string;
12
+ deprecated?: boolean;
13
+ produces?: string[];
14
+ consumes?: string[];
15
+ [key: string]: any; // Allow additional OpenAPI properties
16
+ }
17
+
6
18
  // Configuration interface for route handlers
7
19
  interface RouteConfig {
8
20
  params?: z.ZodSchema<any>;
@@ -10,6 +22,7 @@ interface RouteConfig {
10
22
  body?: z.ZodSchema<any>;
11
23
  headers?: z.ZodSchema<any>;
12
24
  response?: z.ZodSchema<any>;
25
+ detail?: RouteDetail;
13
26
  }
14
27
 
15
28
  // Context type that's fully typed based on the route configuration
@@ -18,6 +31,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
18
31
  query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
19
32
  body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
20
33
  headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
34
+ path: string;
21
35
  request: Request;
22
36
  set: {
23
37
  status?: number;
@@ -39,21 +53,36 @@ interface Route {
39
53
  config?: RouteConfig;
40
54
  }
41
55
 
56
+ // WebSocket handler interface
57
+ interface WebSocketHandler {
58
+ onOpen?: (ws: any) => void;
59
+ onMessage?: (ws: any, message: string | Buffer) => void;
60
+ onClose?: (ws: any, code?: number, reason?: string) => void;
61
+ onError?: (ws: any, error: Error) => void;
62
+ }
63
+
64
+ // WebSocket route definition
65
+ interface WSRoute {
66
+ path: string;
67
+ handler: WebSocketHandler;
68
+ }
69
+
42
70
  // Lifecycle hooks
43
71
  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;
72
+ onBeforeStart?: (instance: BXO) => Promise<void> | void;
73
+ onAfterStart?: (instance: BXO) => Promise<void> | void;
74
+ onBeforeStop?: (instance: BXO) => Promise<void> | void;
75
+ onAfterStop?: (instance: BXO) => Promise<void> | void;
76
+ onBeforeRestart?: (instance: BXO) => Promise<void> | void;
77
+ onAfterRestart?: (instance: BXO) => Promise<void> | void;
78
+ onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
79
+ onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
80
+ onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
53
81
  }
54
82
 
55
83
  export default class BXO {
56
- private routes: Route[] = [];
84
+ private _routes: Route[] = [];
85
+ private _wsRoutes: WSRoute[] = [];
57
86
  private plugins: BXO[] = [];
58
87
  private hooks: LifecycleHooks = {};
59
88
  private server?: any;
@@ -61,51 +90,53 @@ export default class BXO {
61
90
  private hotReloadEnabled: boolean = false;
62
91
  private watchedFiles: Set<string> = new Set();
63
92
  private watchedExclude: Set<string> = new Set();
93
+ private serverPort?: number;
94
+ private serverHostname?: string;
64
95
 
65
96
  constructor() { }
66
97
 
67
98
  // Lifecycle hook methods
68
- onBeforeStart(handler: () => Promise<void> | void): this {
99
+ onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
69
100
  this.hooks.onBeforeStart = handler;
70
101
  return this;
71
102
  }
72
103
 
73
- onAfterStart(handler: () => Promise<void> | void): this {
104
+ onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
74
105
  this.hooks.onAfterStart = handler;
75
106
  return this;
76
107
  }
77
108
 
78
- onBeforeStop(handler: () => Promise<void> | void): this {
109
+ onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
79
110
  this.hooks.onBeforeStop = handler;
80
111
  return this;
81
112
  }
82
113
 
83
- onAfterStop(handler: () => Promise<void> | void): this {
114
+ onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
84
115
  this.hooks.onAfterStop = handler;
85
116
  return this;
86
117
  }
87
118
 
88
- onBeforeRestart(handler: () => Promise<void> | void): this {
119
+ onBeforeRestart(handler: (instance: BXO) => Promise<void> | void): this {
89
120
  this.hooks.onBeforeRestart = handler;
90
121
  return this;
91
122
  }
92
123
 
93
- onAfterRestart(handler: () => Promise<void> | void): this {
124
+ onAfterRestart(handler: (instance: BXO) => Promise<void> | void): this {
94
125
  this.hooks.onAfterRestart = handler;
95
126
  return this;
96
127
  }
97
128
 
98
- onRequest(handler: (ctx: Context) => Promise<void> | void): this {
129
+ onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
99
130
  this.hooks.onRequest = handler;
100
131
  return this;
101
132
  }
102
133
 
103
- onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
134
+ onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
104
135
  this.hooks.onResponse = handler;
105
136
  return this;
106
137
  }
107
138
 
108
- onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
139
+ onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
109
140
  this.hooks.onError = handler;
110
141
  return this;
111
142
  }
@@ -131,7 +162,7 @@ export default class BXO {
131
162
  handler: Handler<TConfig>,
132
163
  config?: TConfig
133
164
  ): this {
134
- this.routes.push({ method: 'GET', path, handler, config });
165
+ this._routes.push({ method: 'GET', path, handler, config });
135
166
  return this;
136
167
  }
137
168
 
@@ -149,7 +180,7 @@ export default class BXO {
149
180
  handler: Handler<TConfig>,
150
181
  config?: TConfig
151
182
  ): this {
152
- this.routes.push({ method: 'POST', path, handler, config });
183
+ this._routes.push({ method: 'POST', path, handler, config });
153
184
  return this;
154
185
  }
155
186
 
@@ -167,7 +198,7 @@ export default class BXO {
167
198
  handler: Handler<TConfig>,
168
199
  config?: TConfig
169
200
  ): this {
170
- this.routes.push({ method: 'PUT', path, handler, config });
201
+ this._routes.push({ method: 'PUT', path, handler, config });
171
202
  return this;
172
203
  }
173
204
 
@@ -185,7 +216,7 @@ export default class BXO {
185
216
  handler: Handler<TConfig>,
186
217
  config?: TConfig
187
218
  ): this {
188
- this.routes.push({ method: 'DELETE', path, handler, config });
219
+ this._routes.push({ method: 'DELETE', path, handler, config });
189
220
  return this;
190
221
  }
191
222
 
@@ -203,28 +234,118 @@ export default class BXO {
203
234
  handler: Handler<TConfig>,
204
235
  config?: TConfig
205
236
  ): this {
206
- this.routes.push({ method: 'PATCH', path, handler, config });
237
+ this._routes.push({ method: 'PATCH', path, handler, config });
238
+ return this;
239
+ }
240
+
241
+ // WebSocket route handler
242
+ ws(path: string, handler: WebSocketHandler): this {
243
+ this._wsRoutes.push({ path, handler });
207
244
  return this;
208
245
  }
209
246
 
210
247
  // Route matching utility
211
248
  private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
212
- for (const route of this.routes) {
249
+ for (const route of this._routes) {
213
250
  if (route.method !== method) continue;
214
251
 
215
252
  const routeSegments = route.path.split('/').filter(Boolean);
216
253
  const pathSegments = pathname.split('/').filter(Boolean);
217
254
 
218
- if (routeSegments.length !== pathSegments.length) continue;
255
+ const params: Record<string, string> = {};
256
+ let isMatch = true;
257
+
258
+ // Handle wildcard at the end (catch-all)
259
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
260
+
261
+ if (hasWildcardAtEnd) {
262
+ // For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
263
+ if (pathSegments.length < routeSegments.length - 1) continue;
264
+ } else {
265
+ // For exact matching (with possible single-segment wildcards), lengths must match
266
+ if (routeSegments.length !== pathSegments.length) continue;
267
+ }
268
+
269
+ for (let i = 0; i < routeSegments.length; i++) {
270
+ const routeSegment = routeSegments[i];
271
+ const pathSegment = pathSegments[i];
272
+
273
+ if (!routeSegment) {
274
+ isMatch = false;
275
+ break;
276
+ }
277
+
278
+ // Handle catch-all wildcard at the end
279
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
280
+ // Wildcard at end matches remaining path segments
281
+ const remainingPath = pathSegments.slice(i).join('/');
282
+ params['*'] = remainingPath;
283
+ break;
284
+ }
285
+
286
+ if (!pathSegment) {
287
+ isMatch = false;
288
+ break;
289
+ }
290
+
291
+ if (routeSegment.startsWith(':')) {
292
+ const paramName = routeSegment.slice(1);
293
+ params[paramName] = decodeURIComponent(pathSegment);
294
+ } else if (routeSegment === '*') {
295
+ // Single segment wildcard
296
+ params['*'] = decodeURIComponent(pathSegment);
297
+ } else if (routeSegment !== pathSegment) {
298
+ isMatch = false;
299
+ break;
300
+ }
301
+ }
302
+
303
+ if (isMatch) {
304
+ return { route, params };
305
+ }
306
+ }
307
+
308
+ return null;
309
+ }
310
+
311
+ // WebSocket route matching utility
312
+ private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
313
+ for (const route of this._wsRoutes) {
314
+ const routeSegments = route.path.split('/').filter(Boolean);
315
+ const pathSegments = pathname.split('/').filter(Boolean);
219
316
 
220
317
  const params: Record<string, string> = {};
221
318
  let isMatch = true;
222
319
 
320
+ // Handle wildcard at the end (catch-all)
321
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
322
+
323
+ if (hasWildcardAtEnd) {
324
+ // For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
325
+ if (pathSegments.length < routeSegments.length - 1) continue;
326
+ } else {
327
+ // For exact matching (with possible single-segment wildcards), lengths must match
328
+ if (routeSegments.length !== pathSegments.length) continue;
329
+ }
330
+
223
331
  for (let i = 0; i < routeSegments.length; i++) {
224
332
  const routeSegment = routeSegments[i];
225
333
  const pathSegment = pathSegments[i];
226
334
 
227
- if (!routeSegment || !pathSegment) {
335
+ if (!routeSegment) {
336
+ isMatch = false;
337
+ break;
338
+ }
339
+
340
+ // Handle catch-all wildcard at the end
341
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
342
+ // Wildcard at end matches remaining path segments
343
+ const remainingPath = pathSegments.slice(i).join('/');
344
+ params['*'] = remainingPath;
345
+ break;
346
+ }
347
+
348
+ if (!pathSegment) {
228
349
  isMatch = false;
229
350
  break;
230
351
  }
@@ -232,6 +353,9 @@ export default class BXO {
232
353
  if (routeSegment.startsWith(':')) {
233
354
  const paramName = routeSegment.slice(1);
234
355
  params[paramName] = decodeURIComponent(pathSegment);
356
+ } else if (routeSegment === '*') {
357
+ // Single segment wildcard
358
+ params['*'] = decodeURIComponent(pathSegment);
235
359
  } else if (routeSegment !== pathSegment) {
236
360
  isMatch = false;
237
361
  break;
@@ -271,11 +395,30 @@ export default class BXO {
271
395
  }
272
396
 
273
397
  // Main request handler
274
- private async handleRequest(request: Request): Promise<Response> {
398
+ private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
275
399
  const url = new URL(request.url);
276
400
  const method = request.method;
277
401
  const pathname = url.pathname;
278
402
 
403
+ // Check for WebSocket upgrade
404
+ if (request.headers.get('upgrade') === 'websocket') {
405
+ const wsMatchResult = this.matchWSRoute(pathname);
406
+ if (wsMatchResult && server) {
407
+ const success = server.upgrade(request, {
408
+ data: {
409
+ handler: wsMatchResult.route.handler,
410
+ params: wsMatchResult.params,
411
+ pathname
412
+ }
413
+ });
414
+
415
+ if (success) {
416
+ return; // undefined response means upgrade was successful
417
+ }
418
+ }
419
+ return new Response('WebSocket upgrade failed', { status: 400 });
420
+ }
421
+
279
422
  const matchResult = this.matchRoute(method, pathname);
280
423
  if (!matchResult) {
281
424
  return new Response('Not Found', { status: 404 });
@@ -308,6 +451,7 @@ export default class BXO {
308
451
  query: route.config?.query ? this.validateData(route.config.query, query) : query,
309
452
  body: route.config?.body ? this.validateData(route.config.body, body) : body,
310
453
  headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
454
+ path: pathname,
311
455
  request,
312
456
  set: {}
313
457
  };
@@ -315,13 +459,13 @@ export default class BXO {
315
459
  try {
316
460
  // Run global onRequest hook
317
461
  if (this.hooks.onRequest) {
318
- await this.hooks.onRequest(ctx);
462
+ await this.hooks.onRequest(ctx, this);
319
463
  }
320
464
 
321
465
  // Run BXO instance onRequest hooks
322
466
  for (const bxoInstance of this.plugins) {
323
467
  if (bxoInstance.hooks.onRequest) {
324
- await bxoInstance.hooks.onRequest(ctx);
468
+ await bxoInstance.hooks.onRequest(ctx, this);
325
469
  }
326
470
  }
327
471
 
@@ -330,13 +474,13 @@ export default class BXO {
330
474
 
331
475
  // Run global onResponse hook
332
476
  if (this.hooks.onResponse) {
333
- response = await this.hooks.onResponse(ctx, response) || response;
477
+ response = await this.hooks.onResponse(ctx, response, this) || response;
334
478
  }
335
479
 
336
480
  // Run BXO instance onResponse hooks
337
481
  for (const bxoInstance of this.plugins) {
338
482
  if (bxoInstance.hooks.onResponse) {
339
- response = await bxoInstance.hooks.onResponse(ctx, response) || response;
483
+ response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
340
484
  }
341
485
  }
342
486
 
@@ -359,6 +503,35 @@ export default class BXO {
359
503
  return response;
360
504
  }
361
505
 
506
+ // Handle File response (like Elysia)
507
+ if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
508
+ const file = response as File;
509
+ const responseInit: ResponseInit = {
510
+ status: ctx.set.status || 200,
511
+ headers: {
512
+ 'Content-Type': file.type || 'application/octet-stream',
513
+ 'Content-Length': file.size.toString(),
514
+ ...ctx.set.headers
515
+ }
516
+ };
517
+ return new Response(file, responseInit);
518
+ }
519
+
520
+ // Handle Bun.file() response
521
+ if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
522
+ const bunFile = response as any;
523
+ const responseInit: ResponseInit = {
524
+ status: ctx.set.status || 200,
525
+ headers: {
526
+ 'Content-Type': bunFile.type || 'application/octet-stream',
527
+ 'Content-Length': bunFile.size?.toString() || '',
528
+ ...ctx.set.headers,
529
+ ...(bunFile.headers || {}) // Support custom headers from file helper
530
+ }
531
+ };
532
+ return new Response(bunFile, responseInit);
533
+ }
534
+
362
535
  const responseInit: ResponseInit = {
363
536
  status: ctx.set.status || 200,
364
537
  headers: ctx.set.headers || {}
@@ -381,12 +554,12 @@ export default class BXO {
381
554
  let errorResponse: any;
382
555
 
383
556
  if (this.hooks.onError) {
384
- errorResponse = await this.hooks.onError(ctx, error as Error);
557
+ errorResponse = await this.hooks.onError(ctx, error as Error, this);
385
558
  }
386
559
 
387
560
  for (const bxoInstance of this.plugins) {
388
561
  if (bxoInstance.hooks.onError) {
389
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
562
+ errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
390
563
  }
391
564
  }
392
565
 
@@ -423,14 +596,14 @@ export default class BXO {
423
596
  if (pattern === filename) {
424
597
  return true;
425
598
  }
426
-
599
+
427
600
  // Handle directory patterns (e.g., "node_modules/", "dist/")
428
601
  if (pattern.endsWith('/')) {
429
602
  if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
430
603
  return true;
431
604
  }
432
605
  }
433
-
606
+
434
607
  // Handle wildcard patterns (e.g., "*.log", "temp*")
435
608
  if (pattern.includes('*')) {
436
609
  const regex = new RegExp(pattern.replace(/\*/g, '.*'));
@@ -438,18 +611,18 @@ export default class BXO {
438
611
  return true;
439
612
  }
440
613
  }
441
-
614
+
442
615
  // Handle file extension patterns (e.g., ".log", ".tmp")
443
616
  if (pattern.startsWith('.') && filename.endsWith(pattern)) {
444
617
  return true;
445
618
  }
446
-
619
+
447
620
  // Handle substring matches for directories
448
621
  if (filename.includes(pattern)) {
449
622
  return true;
450
623
  }
451
624
  }
452
-
625
+
453
626
  return false;
454
627
  }
455
628
 
@@ -466,7 +639,7 @@ export default class BXO {
466
639
  if (this.shouldExcludeFile(filename)) {
467
640
  return;
468
641
  }
469
-
642
+
470
643
  console.log(`🔄 File changed: ${filename}, restarting server...`);
471
644
  await this.restart(port, hostname);
472
645
  }
@@ -491,22 +664,44 @@ export default class BXO {
491
664
  try {
492
665
  // Before start hook
493
666
  if (this.hooks.onBeforeStart) {
494
- await this.hooks.onBeforeStart();
667
+ await this.hooks.onBeforeStart(this);
495
668
  }
496
669
 
497
670
  this.server = Bun.serve({
498
671
  port,
499
672
  hostname,
500
- fetch: (request) => this.handleRequest(request),
673
+ fetch: (request, server) => this.handleRequest(request, server),
674
+ websocket: {
675
+ message: (ws: any, message: any) => {
676
+ const handler = ws.data?.handler;
677
+ if (handler?.onMessage) {
678
+ handler.onMessage(ws, message);
679
+ }
680
+ },
681
+ open: (ws: any) => {
682
+ const handler = ws.data?.handler;
683
+ if (handler?.onOpen) {
684
+ handler.onOpen(ws);
685
+ }
686
+ },
687
+ close: (ws: any, code?: number, reason?: string) => {
688
+ const handler = ws.data?.handler;
689
+ if (handler?.onClose) {
690
+ handler.onClose(ws, code, reason);
691
+ }
692
+ }
693
+ }
501
694
  });
502
695
 
503
696
  this.isRunning = true;
697
+ this.serverPort = port;
698
+ this.serverHostname = hostname;
504
699
 
505
700
  console.log(`🦊 BXO server running at http://${hostname}:${port}`);
506
701
 
507
702
  // After start hook
508
703
  if (this.hooks.onAfterStart) {
509
- await this.hooks.onAfterStart();
704
+ await this.hooks.onAfterStart(this);
510
705
  }
511
706
 
512
707
  // Setup hot reload
@@ -536,7 +731,7 @@ export default class BXO {
536
731
  try {
537
732
  // Before stop hook
538
733
  if (this.hooks.onBeforeStop) {
539
- await this.hooks.onBeforeStop();
734
+ await this.hooks.onBeforeStop(this);
540
735
  }
541
736
 
542
737
  if (this.server) {
@@ -545,12 +740,14 @@ export default class BXO {
545
740
  }
546
741
 
547
742
  this.isRunning = false;
743
+ this.serverPort = undefined;
744
+ this.serverHostname = undefined;
548
745
 
549
746
  console.log('🛑 BXO server stopped');
550
747
 
551
748
  // After stop hook
552
749
  if (this.hooks.onAfterStop) {
553
- await this.hooks.onAfterStop();
750
+ await this.hooks.onAfterStop(this);
554
751
  }
555
752
 
556
753
  } catch (error) {
@@ -563,7 +760,7 @@ export default class BXO {
563
760
  try {
564
761
  // Before restart hook
565
762
  if (this.hooks.onBeforeRestart) {
566
- await this.hooks.onBeforeRestart();
763
+ await this.hooks.onBeforeRestart(this);
567
764
  }
568
765
 
569
766
  console.log('🔄 Restarting BXO server...');
@@ -577,7 +774,7 @@ export default class BXO {
577
774
 
578
775
  // After restart hook
579
776
  if (this.hooks.onAfterRestart) {
580
- await this.hooks.onAfterRestart();
777
+ await this.hooks.onAfterRestart(this);
581
778
  }
582
779
 
583
780
  } catch (error) {
@@ -604,14 +801,85 @@ export default class BXO {
604
801
  excludePatterns: Array.from(this.watchedExclude)
605
802
  };
606
803
  }
804
+
805
+ // Get server information (alias for getServerInfo)
806
+ get info() {
807
+ return {
808
+ // Server status
809
+ running: this.isRunning,
810
+ server: this.server ? 'Bun' : null,
811
+
812
+ // Connection details
813
+ hostname: this.serverHostname,
814
+ port: this.serverPort,
815
+ url: this.isRunning && this.serverHostname && this.serverPort
816
+ ? `http://${this.serverHostname}:${this.serverPort}`
817
+ : null,
818
+
819
+ // Application statistics
820
+ totalRoutes: this._routes.length,
821
+ totalWsRoutes: this._wsRoutes.length,
822
+ totalPlugins: this.plugins.length,
823
+
824
+ // Hot reload configuration
825
+ hotReload: this.hotReloadEnabled,
826
+ watchedFiles: Array.from(this.watchedFiles),
827
+ excludePatterns: Array.from(this.watchedExclude),
828
+
829
+ // System information
830
+ runtime: 'Bun',
831
+ version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
832
+ pid: process.pid,
833
+ uptime: this.isRunning ? process.uptime() : 0
834
+ };
835
+ }
836
+
837
+ // Get all routes information
838
+ get routes() {
839
+ return this._routes.map((route: Route) => ({
840
+ method: route.method,
841
+ path: route.path,
842
+ hasConfig: !!route.config,
843
+ config: route.config || null
844
+ }));
845
+ }
846
+
847
+ // Get all WebSocket routes information
848
+ get wsRoutes() {
849
+ return this._wsRoutes.map((route: WSRoute) => ({
850
+ path: route.path,
851
+ hasHandlers: {
852
+ onOpen: !!route.handler.onOpen,
853
+ onMessage: !!route.handler.onMessage,
854
+ onClose: !!route.handler.onClose,
855
+ onError: !!route.handler.onError
856
+ }
857
+ }));
858
+ }
859
+ }
860
+
861
+ const error = (error: Error | string, status: number = 500) => {
862
+ return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
607
863
  }
608
864
 
609
- const error = (error: Error, status: number = 500) => {
610
- return new Response(JSON.stringify({ error: error.message }), { status });
865
+ // File helper function (like Elysia)
866
+ const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
867
+ const bunFile = Bun.file(path);
868
+
869
+ if (options?.type) {
870
+ // Create a wrapper to override the MIME type
871
+ return {
872
+ ...bunFile,
873
+ type: options.type,
874
+ headers: options.headers
875
+ };
876
+ }
877
+
878
+ return bunFile;
611
879
  }
612
880
 
613
881
  // Export Zod for convenience
614
- export { z, error };
882
+ export { z, error, file };
615
883
 
616
884
  // Export types for external use
617
- export type { RouteConfig, Handler };
885
+ export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.1",
4
+ "version": "0.0.5-dev.10",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {