bxo 0.0.4 → 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 +378 -48
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -3,12 +3,26 @@ 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>;
9
21
  query?: z.ZodSchema<any>;
10
22
  body?: z.ZodSchema<any>;
11
23
  headers?: z.ZodSchema<any>;
24
+ response?: z.ZodSchema<any>;
25
+ detail?: RouteDetail;
12
26
  }
13
27
 
14
28
  // Context type that's fully typed based on the route configuration
@@ -17,6 +31,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
17
31
  query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
18
32
  body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
19
33
  headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
34
+ path: string;
20
35
  request: Request;
21
36
  set: {
22
37
  status?: number;
@@ -38,72 +53,90 @@ interface Route {
38
53
  config?: RouteConfig;
39
54
  }
40
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
+
41
70
  // Lifecycle hooks
42
71
  interface LifecycleHooks {
43
- onBeforeStart?: () => Promise<void> | void;
44
- onAfterStart?: () => Promise<void> | void;
45
- onBeforeStop?: () => Promise<void> | void;
46
- onAfterStop?: () => Promise<void> | void;
47
- onBeforeRestart?: () => Promise<void> | void;
48
- onAfterRestart?: () => Promise<void> | void;
49
- onRequest?: (ctx: Context) => Promise<void> | void;
50
- onResponse?: (ctx: Context, response: any) => Promise<any> | any;
51
- 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;
52
81
  }
53
82
 
54
83
  export default class BXO {
55
- private routes: Route[] = [];
84
+ private _routes: Route[] = [];
85
+ private _wsRoutes: WSRoute[] = [];
56
86
  private plugins: BXO[] = [];
57
87
  private hooks: LifecycleHooks = {};
58
88
  private server?: any;
59
89
  private isRunning: boolean = false;
60
90
  private hotReloadEnabled: boolean = false;
61
91
  private watchedFiles: Set<string> = new Set();
92
+ private watchedExclude: Set<string> = new Set();
93
+ private serverPort?: number;
94
+ private serverHostname?: string;
62
95
 
63
96
  constructor() { }
64
97
 
65
98
  // Lifecycle hook methods
66
- onBeforeStart(handler: () => Promise<void> | void): this {
99
+ onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
67
100
  this.hooks.onBeforeStart = handler;
68
101
  return this;
69
102
  }
70
103
 
71
- onAfterStart(handler: () => Promise<void> | void): this {
104
+ onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
72
105
  this.hooks.onAfterStart = handler;
73
106
  return this;
74
107
  }
75
108
 
76
- onBeforeStop(handler: () => Promise<void> | void): this {
109
+ onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
77
110
  this.hooks.onBeforeStop = handler;
78
111
  return this;
79
112
  }
80
113
 
81
- onAfterStop(handler: () => Promise<void> | void): this {
114
+ onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
82
115
  this.hooks.onAfterStop = handler;
83
116
  return this;
84
117
  }
85
118
 
86
- onBeforeRestart(handler: () => Promise<void> | void): this {
119
+ onBeforeRestart(handler: (instance: BXO) => Promise<void> | void): this {
87
120
  this.hooks.onBeforeRestart = handler;
88
121
  return this;
89
122
  }
90
123
 
91
- onAfterRestart(handler: () => Promise<void> | void): this {
124
+ onAfterRestart(handler: (instance: BXO) => Promise<void> | void): this {
92
125
  this.hooks.onAfterRestart = handler;
93
126
  return this;
94
127
  }
95
128
 
96
- onRequest(handler: (ctx: Context) => Promise<void> | void): this {
129
+ onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
97
130
  this.hooks.onRequest = handler;
98
131
  return this;
99
132
  }
100
133
 
101
- onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
134
+ onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
102
135
  this.hooks.onResponse = handler;
103
136
  return this;
104
137
  }
105
138
 
106
- onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
139
+ onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
107
140
  this.hooks.onError = handler;
108
141
  return this;
109
142
  }
@@ -129,7 +162,7 @@ export default class BXO {
129
162
  handler: Handler<TConfig>,
130
163
  config?: TConfig
131
164
  ): this {
132
- this.routes.push({ method: 'GET', path, handler, config });
165
+ this._routes.push({ method: 'GET', path, handler, config });
133
166
  return this;
134
167
  }
135
168
 
@@ -147,7 +180,7 @@ export default class BXO {
147
180
  handler: Handler<TConfig>,
148
181
  config?: TConfig
149
182
  ): this {
150
- this.routes.push({ method: 'POST', path, handler, config });
183
+ this._routes.push({ method: 'POST', path, handler, config });
151
184
  return this;
152
185
  }
153
186
 
@@ -165,7 +198,7 @@ export default class BXO {
165
198
  handler: Handler<TConfig>,
166
199
  config?: TConfig
167
200
  ): this {
168
- this.routes.push({ method: 'PUT', path, handler, config });
201
+ this._routes.push({ method: 'PUT', path, handler, config });
169
202
  return this;
170
203
  }
171
204
 
@@ -183,7 +216,7 @@ export default class BXO {
183
216
  handler: Handler<TConfig>,
184
217
  config?: TConfig
185
218
  ): this {
186
- this.routes.push({ method: 'DELETE', path, handler, config });
219
+ this._routes.push({ method: 'DELETE', path, handler, config });
187
220
  return this;
188
221
  }
189
222
 
@@ -201,28 +234,118 @@ export default class BXO {
201
234
  handler: Handler<TConfig>,
202
235
  config?: TConfig
203
236
  ): this {
204
- 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 });
205
244
  return this;
206
245
  }
207
246
 
208
247
  // Route matching utility
209
248
  private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
210
- for (const route of this.routes) {
249
+ for (const route of this._routes) {
211
250
  if (route.method !== method) continue;
212
251
 
213
252
  const routeSegments = route.path.split('/').filter(Boolean);
214
253
  const pathSegments = pathname.split('/').filter(Boolean);
215
254
 
216
- 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);
217
316
 
218
317
  const params: Record<string, string> = {};
219
318
  let isMatch = true;
220
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
+
221
331
  for (let i = 0; i < routeSegments.length; i++) {
222
332
  const routeSegment = routeSegments[i];
223
333
  const pathSegment = pathSegments[i];
224
334
 
225
- 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) {
226
349
  isMatch = false;
227
350
  break;
228
351
  }
@@ -230,6 +353,9 @@ export default class BXO {
230
353
  if (routeSegment.startsWith(':')) {
231
354
  const paramName = routeSegment.slice(1);
232
355
  params[paramName] = decodeURIComponent(pathSegment);
356
+ } else if (routeSegment === '*') {
357
+ // Single segment wildcard
358
+ params['*'] = decodeURIComponent(pathSegment);
233
359
  } else if (routeSegment !== pathSegment) {
234
360
  isMatch = false;
235
361
  break;
@@ -269,11 +395,30 @@ export default class BXO {
269
395
  }
270
396
 
271
397
  // Main request handler
272
- private async handleRequest(request: Request): Promise<Response> {
398
+ private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
273
399
  const url = new URL(request.url);
274
400
  const method = request.method;
275
401
  const pathname = url.pathname;
276
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
+
277
422
  const matchResult = this.matchRoute(method, pathname);
278
423
  if (!matchResult) {
279
424
  return new Response('Not Found', { status: 404 });
@@ -306,6 +451,7 @@ export default class BXO {
306
451
  query: route.config?.query ? this.validateData(route.config.query, query) : query,
307
452
  body: route.config?.body ? this.validateData(route.config.body, body) : body,
308
453
  headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
454
+ path: pathname,
309
455
  request,
310
456
  set: {}
311
457
  };
@@ -313,13 +459,13 @@ export default class BXO {
313
459
  try {
314
460
  // Run global onRequest hook
315
461
  if (this.hooks.onRequest) {
316
- await this.hooks.onRequest(ctx);
462
+ await this.hooks.onRequest(ctx, this);
317
463
  }
318
464
 
319
465
  // Run BXO instance onRequest hooks
320
466
  for (const bxoInstance of this.plugins) {
321
467
  if (bxoInstance.hooks.onRequest) {
322
- await bxoInstance.hooks.onRequest(ctx);
468
+ await bxoInstance.hooks.onRequest(ctx, this);
323
469
  }
324
470
  }
325
471
 
@@ -328,13 +474,27 @@ export default class BXO {
328
474
 
329
475
  // Run global onResponse hook
330
476
  if (this.hooks.onResponse) {
331
- response = await this.hooks.onResponse(ctx, response) || response;
477
+ response = await this.hooks.onResponse(ctx, response, this) || response;
332
478
  }
333
479
 
334
480
  // Run BXO instance onResponse hooks
335
481
  for (const bxoInstance of this.plugins) {
336
482
  if (bxoInstance.hooks.onResponse) {
337
- response = await bxoInstance.hooks.onResponse(ctx, response) || response;
483
+ response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
484
+ }
485
+ }
486
+
487
+ // Validate response against schema if provided
488
+ if (route.config?.response && !(response instanceof Response)) {
489
+ try {
490
+ response = this.validateData(route.config.response, response);
491
+ } catch (validationError) {
492
+ // Response validation failed
493
+ const errorMessage = validationError instanceof Error ? validationError.message : 'Response validation failed';
494
+ return new Response(JSON.stringify({ error: `Response validation error: ${errorMessage}` }), {
495
+ status: 500,
496
+ headers: { 'Content-Type': 'application/json' }
497
+ });
338
498
  }
339
499
  }
340
500
 
@@ -343,6 +503,35 @@ export default class BXO {
343
503
  return response;
344
504
  }
345
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
+
346
535
  const responseInit: ResponseInit = {
347
536
  status: ctx.set.status || 200,
348
537
  headers: ctx.set.headers || {}
@@ -365,12 +554,12 @@ export default class BXO {
365
554
  let errorResponse: any;
366
555
 
367
556
  if (this.hooks.onError) {
368
- errorResponse = await this.hooks.onError(ctx, error as Error);
557
+ errorResponse = await this.hooks.onError(ctx, error as Error, this);
369
558
  }
370
559
 
371
560
  for (const bxoInstance of this.plugins) {
372
561
  if (bxoInstance.hooks.onError) {
373
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
562
+ errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
374
563
  }
375
564
  }
376
565
 
@@ -394,12 +583,49 @@ export default class BXO {
394
583
  }
395
584
 
396
585
  // Hot reload functionality
397
- enableHotReload(watchPaths: string[] = ['./']): this {
586
+ enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
398
587
  this.hotReloadEnabled = true;
399
588
  watchPaths.forEach(path => this.watchedFiles.add(path));
589
+ excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
400
590
  return this;
401
591
  }
402
592
 
593
+ private shouldExcludeFile(filename: string): boolean {
594
+ for (const pattern of this.watchedExclude) {
595
+ // Handle exact match
596
+ if (pattern === filename) {
597
+ return true;
598
+ }
599
+
600
+ // Handle directory patterns (e.g., "node_modules/", "dist/")
601
+ if (pattern.endsWith('/')) {
602
+ if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
603
+ return true;
604
+ }
605
+ }
606
+
607
+ // Handle wildcard patterns (e.g., "*.log", "temp*")
608
+ if (pattern.includes('*')) {
609
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
610
+ if (regex.test(filename)) {
611
+ return true;
612
+ }
613
+ }
614
+
615
+ // Handle file extension patterns (e.g., ".log", ".tmp")
616
+ if (pattern.startsWith('.') && filename.endsWith(pattern)) {
617
+ return true;
618
+ }
619
+
620
+ // Handle substring matches for directories
621
+ if (filename.includes(pattern)) {
622
+ return true;
623
+ }
624
+ }
625
+
626
+ return false;
627
+ }
628
+
403
629
  private async setupFileWatcher(port: number, hostname: string): Promise<void> {
404
630
  if (!this.hotReloadEnabled) return;
405
631
 
@@ -409,11 +635,19 @@ export default class BXO {
409
635
  try {
410
636
  fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
411
637
  if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
638
+ // Check if file should be excluded
639
+ if (this.shouldExcludeFile(filename)) {
640
+ return;
641
+ }
642
+
412
643
  console.log(`🔄 File changed: ${filename}, restarting server...`);
413
644
  await this.restart(port, hostname);
414
645
  }
415
646
  });
416
647
  console.log(`👀 Watching ${watchPath} for changes...`);
648
+ if (this.watchedExclude.size > 0) {
649
+ console.log(`🚫 Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
650
+ }
417
651
  } catch (error) {
418
652
  console.warn(`⚠️ Could not watch ${watchPath}:`, error);
419
653
  }
@@ -430,22 +664,44 @@ export default class BXO {
430
664
  try {
431
665
  // Before start hook
432
666
  if (this.hooks.onBeforeStart) {
433
- await this.hooks.onBeforeStart();
667
+ await this.hooks.onBeforeStart(this);
434
668
  }
435
669
 
436
670
  this.server = Bun.serve({
437
671
  port,
438
672
  hostname,
439
- 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
+ }
440
694
  });
441
695
 
442
696
  this.isRunning = true;
697
+ this.serverPort = port;
698
+ this.serverHostname = hostname;
443
699
 
444
700
  console.log(`🦊 BXO server running at http://${hostname}:${port}`);
445
701
 
446
702
  // After start hook
447
703
  if (this.hooks.onAfterStart) {
448
- await this.hooks.onAfterStart();
704
+ await this.hooks.onAfterStart(this);
449
705
  }
450
706
 
451
707
  // Setup hot reload
@@ -475,7 +731,7 @@ export default class BXO {
475
731
  try {
476
732
  // Before stop hook
477
733
  if (this.hooks.onBeforeStop) {
478
- await this.hooks.onBeforeStop();
734
+ await this.hooks.onBeforeStop(this);
479
735
  }
480
736
 
481
737
  if (this.server) {
@@ -484,12 +740,14 @@ export default class BXO {
484
740
  }
485
741
 
486
742
  this.isRunning = false;
743
+ this.serverPort = undefined;
744
+ this.serverHostname = undefined;
487
745
 
488
746
  console.log('🛑 BXO server stopped');
489
747
 
490
748
  // After stop hook
491
749
  if (this.hooks.onAfterStop) {
492
- await this.hooks.onAfterStop();
750
+ await this.hooks.onAfterStop(this);
493
751
  }
494
752
 
495
753
  } catch (error) {
@@ -502,7 +760,7 @@ export default class BXO {
502
760
  try {
503
761
  // Before restart hook
504
762
  if (this.hooks.onBeforeRestart) {
505
- await this.hooks.onBeforeRestart();
763
+ await this.hooks.onBeforeRestart(this);
506
764
  }
507
765
 
508
766
  console.log('🔄 Restarting BXO server...');
@@ -516,7 +774,7 @@ export default class BXO {
516
774
 
517
775
  // After restart hook
518
776
  if (this.hooks.onAfterRestart) {
519
- await this.hooks.onAfterRestart();
777
+ await this.hooks.onAfterRestart(this);
520
778
  }
521
779
 
522
780
  } catch (error) {
@@ -535,21 +793,93 @@ export default class BXO {
535
793
  return this.isRunning;
536
794
  }
537
795
 
538
- getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[] } {
796
+ getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
539
797
  return {
540
798
  running: this.isRunning,
541
799
  hotReload: this.hotReloadEnabled,
542
- watchedFiles: Array.from(this.watchedFiles)
800
+ watchedFiles: Array.from(this.watchedFiles),
801
+ excludePatterns: Array.from(this.watchedExclude)
543
802
  };
544
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 });
545
863
  }
546
864
 
547
- const error = (error: Error, status: number = 500) => {
548
- 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;
549
879
  }
550
880
 
551
881
  // Export Zod for convenience
552
- export { z, error };
882
+ export { z, error, file };
553
883
 
554
884
  // Export types for external use
555
- export type { RouteConfig };
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.4",
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": {