bxo 0.0.5-dev.1 → 0.0.5-dev.11

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 (3) hide show
  1. package/example.ts +1 -1
  2. package/index.ts +342 -158
  3. package/package.json +1 -1
package/example.ts CHANGED
@@ -5,7 +5,7 @@ import { cors, logger, auth, rateLimit, createJWT } from './plugins';
5
5
  const app = new BXO();
6
6
 
7
7
  // Enable hot reload
8
- app.enableHotReload(['./']); // Watch current directory
8
+ app.enableHotReload([process.cwd(), './']); // Watch current directory
9
9
 
10
10
  // Add plugins
11
11
  app
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,73 +53,77 @@ 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
+ onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
77
+ onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
78
+ onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
53
79
  }
54
80
 
55
81
  export default class BXO {
56
- private routes: Route[] = [];
82
+ private _routes: Route[] = [];
83
+ private _wsRoutes: WSRoute[] = [];
57
84
  private plugins: BXO[] = [];
58
85
  private hooks: LifecycleHooks = {};
59
86
  private server?: any;
60
87
  private isRunning: boolean = false;
61
- private hotReloadEnabled: boolean = false;
62
- private watchedFiles: Set<string> = new Set();
63
- private watchedExclude: Set<string> = new Set();
88
+ private serverPort?: number;
89
+ private serverHostname?: string;
64
90
 
65
91
  constructor() { }
66
92
 
67
93
  // Lifecycle hook methods
68
- onBeforeStart(handler: () => Promise<void> | void): this {
94
+ onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
69
95
  this.hooks.onBeforeStart = handler;
70
96
  return this;
71
97
  }
72
98
 
73
- onAfterStart(handler: () => Promise<void> | void): this {
99
+ onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
74
100
  this.hooks.onAfterStart = handler;
75
101
  return this;
76
102
  }
77
103
 
78
- onBeforeStop(handler: () => Promise<void> | void): this {
104
+ onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
79
105
  this.hooks.onBeforeStop = handler;
80
106
  return this;
81
107
  }
82
108
 
83
- onAfterStop(handler: () => Promise<void> | void): this {
109
+ onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
84
110
  this.hooks.onAfterStop = handler;
85
111
  return this;
86
112
  }
87
113
 
88
- onBeforeRestart(handler: () => Promise<void> | void): this {
89
- this.hooks.onBeforeRestart = handler;
90
- return this;
91
- }
92
114
 
93
- onAfterRestart(handler: () => Promise<void> | void): this {
94
- this.hooks.onAfterRestart = handler;
95
- return this;
96
- }
97
115
 
98
- onRequest(handler: (ctx: Context) => Promise<void> | void): this {
116
+ onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
99
117
  this.hooks.onRequest = handler;
100
118
  return this;
101
119
  }
102
120
 
103
- onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
121
+ onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
104
122
  this.hooks.onResponse = handler;
105
123
  return this;
106
124
  }
107
125
 
108
- onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
126
+ onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
109
127
  this.hooks.onError = handler;
110
128
  return this;
111
129
  }
@@ -131,7 +149,7 @@ export default class BXO {
131
149
  handler: Handler<TConfig>,
132
150
  config?: TConfig
133
151
  ): this {
134
- this.routes.push({ method: 'GET', path, handler, config });
152
+ this._routes.push({ method: 'GET', path, handler, config });
135
153
  return this;
136
154
  }
137
155
 
@@ -149,7 +167,7 @@ export default class BXO {
149
167
  handler: Handler<TConfig>,
150
168
  config?: TConfig
151
169
  ): this {
152
- this.routes.push({ method: 'POST', path, handler, config });
170
+ this._routes.push({ method: 'POST', path, handler, config });
153
171
  return this;
154
172
  }
155
173
 
@@ -167,7 +185,7 @@ export default class BXO {
167
185
  handler: Handler<TConfig>,
168
186
  config?: TConfig
169
187
  ): this {
170
- this.routes.push({ method: 'PUT', path, handler, config });
188
+ this._routes.push({ method: 'PUT', path, handler, config });
171
189
  return this;
172
190
  }
173
191
 
@@ -185,7 +203,7 @@ export default class BXO {
185
203
  handler: Handler<TConfig>,
186
204
  config?: TConfig
187
205
  ): this {
188
- this.routes.push({ method: 'DELETE', path, handler, config });
206
+ this._routes.push({ method: 'DELETE', path, handler, config });
189
207
  return this;
190
208
  }
191
209
 
@@ -203,28 +221,118 @@ export default class BXO {
203
221
  handler: Handler<TConfig>,
204
222
  config?: TConfig
205
223
  ): this {
206
- this.routes.push({ method: 'PATCH', path, handler, config });
224
+ this._routes.push({ method: 'PATCH', path, handler, config });
225
+ return this;
226
+ }
227
+
228
+ // WebSocket route handler
229
+ ws(path: string, handler: WebSocketHandler): this {
230
+ this._wsRoutes.push({ path, handler });
207
231
  return this;
208
232
  }
209
233
 
210
234
  // Route matching utility
211
235
  private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
212
- for (const route of this.routes) {
236
+ for (const route of this._routes) {
213
237
  if (route.method !== method) continue;
214
238
 
215
239
  const routeSegments = route.path.split('/').filter(Boolean);
216
240
  const pathSegments = pathname.split('/').filter(Boolean);
217
241
 
218
- if (routeSegments.length !== pathSegments.length) continue;
242
+ const params: Record<string, string> = {};
243
+ let isMatch = true;
244
+
245
+ // Handle wildcard at the end (catch-all)
246
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
247
+
248
+ if (hasWildcardAtEnd) {
249
+ // For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
250
+ if (pathSegments.length < routeSegments.length - 1) continue;
251
+ } else {
252
+ // For exact matching (with possible single-segment wildcards), lengths must match
253
+ if (routeSegments.length !== pathSegments.length) continue;
254
+ }
255
+
256
+ for (let i = 0; i < routeSegments.length; i++) {
257
+ const routeSegment = routeSegments[i];
258
+ const pathSegment = pathSegments[i];
259
+
260
+ if (!routeSegment) {
261
+ isMatch = false;
262
+ break;
263
+ }
264
+
265
+ // Handle catch-all wildcard at the end
266
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
267
+ // Wildcard at end matches remaining path segments
268
+ const remainingPath = pathSegments.slice(i).join('/');
269
+ params['*'] = remainingPath;
270
+ break;
271
+ }
272
+
273
+ if (!pathSegment) {
274
+ isMatch = false;
275
+ break;
276
+ }
277
+
278
+ if (routeSegment.startsWith(':')) {
279
+ const paramName = routeSegment.slice(1);
280
+ params[paramName] = decodeURIComponent(pathSegment);
281
+ } else if (routeSegment === '*') {
282
+ // Single segment wildcard
283
+ params['*'] = decodeURIComponent(pathSegment);
284
+ } else if (routeSegment !== pathSegment) {
285
+ isMatch = false;
286
+ break;
287
+ }
288
+ }
289
+
290
+ if (isMatch) {
291
+ return { route, params };
292
+ }
293
+ }
294
+
295
+ return null;
296
+ }
297
+
298
+ // WebSocket route matching utility
299
+ private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
300
+ for (const route of this._wsRoutes) {
301
+ const routeSegments = route.path.split('/').filter(Boolean);
302
+ const pathSegments = pathname.split('/').filter(Boolean);
219
303
 
220
304
  const params: Record<string, string> = {};
221
305
  let isMatch = true;
222
306
 
307
+ // Handle wildcard at the end (catch-all)
308
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
309
+
310
+ if (hasWildcardAtEnd) {
311
+ // For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
312
+ if (pathSegments.length < routeSegments.length - 1) continue;
313
+ } else {
314
+ // For exact matching (with possible single-segment wildcards), lengths must match
315
+ if (routeSegments.length !== pathSegments.length) continue;
316
+ }
317
+
223
318
  for (let i = 0; i < routeSegments.length; i++) {
224
319
  const routeSegment = routeSegments[i];
225
320
  const pathSegment = pathSegments[i];
226
321
 
227
- if (!routeSegment || !pathSegment) {
322
+ if (!routeSegment) {
323
+ isMatch = false;
324
+ break;
325
+ }
326
+
327
+ // Handle catch-all wildcard at the end
328
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
329
+ // Wildcard at end matches remaining path segments
330
+ const remainingPath = pathSegments.slice(i).join('/');
331
+ params['*'] = remainingPath;
332
+ break;
333
+ }
334
+
335
+ if (!pathSegment) {
228
336
  isMatch = false;
229
337
  break;
230
338
  }
@@ -232,6 +340,9 @@ export default class BXO {
232
340
  if (routeSegment.startsWith(':')) {
233
341
  const paramName = routeSegment.slice(1);
234
342
  params[paramName] = decodeURIComponent(pathSegment);
343
+ } else if (routeSegment === '*') {
344
+ // Single segment wildcard
345
+ params['*'] = decodeURIComponent(pathSegment);
235
346
  } else if (routeSegment !== pathSegment) {
236
347
  isMatch = false;
237
348
  break;
@@ -271,11 +382,30 @@ export default class BXO {
271
382
  }
272
383
 
273
384
  // Main request handler
274
- private async handleRequest(request: Request): Promise<Response> {
385
+ private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
275
386
  const url = new URL(request.url);
276
387
  const method = request.method;
277
388
  const pathname = url.pathname;
278
389
 
390
+ // Check for WebSocket upgrade
391
+ if (request.headers.get('upgrade') === 'websocket') {
392
+ const wsMatchResult = this.matchWSRoute(pathname);
393
+ if (wsMatchResult && server) {
394
+ const success = server.upgrade(request, {
395
+ data: {
396
+ handler: wsMatchResult.route.handler,
397
+ params: wsMatchResult.params,
398
+ pathname
399
+ }
400
+ });
401
+
402
+ if (success) {
403
+ return; // undefined response means upgrade was successful
404
+ }
405
+ }
406
+ return new Response('WebSocket upgrade failed', { status: 400 });
407
+ }
408
+
279
409
  const matchResult = this.matchRoute(method, pathname);
280
410
  if (!matchResult) {
281
411
  return new Response('Not Found', { status: 404 });
@@ -308,6 +438,7 @@ export default class BXO {
308
438
  query: route.config?.query ? this.validateData(route.config.query, query) : query,
309
439
  body: route.config?.body ? this.validateData(route.config.body, body) : body,
310
440
  headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
441
+ path: pathname,
311
442
  request,
312
443
  set: {}
313
444
  };
@@ -315,13 +446,13 @@ export default class BXO {
315
446
  try {
316
447
  // Run global onRequest hook
317
448
  if (this.hooks.onRequest) {
318
- await this.hooks.onRequest(ctx);
449
+ await this.hooks.onRequest(ctx, this);
319
450
  }
320
451
 
321
452
  // Run BXO instance onRequest hooks
322
453
  for (const bxoInstance of this.plugins) {
323
454
  if (bxoInstance.hooks.onRequest) {
324
- await bxoInstance.hooks.onRequest(ctx);
455
+ await bxoInstance.hooks.onRequest(ctx, this);
325
456
  }
326
457
  }
327
458
 
@@ -330,13 +461,13 @@ export default class BXO {
330
461
 
331
462
  // Run global onResponse hook
332
463
  if (this.hooks.onResponse) {
333
- response = await this.hooks.onResponse(ctx, response) || response;
464
+ response = await this.hooks.onResponse(ctx, response, this) || response;
334
465
  }
335
466
 
336
467
  // Run BXO instance onResponse hooks
337
468
  for (const bxoInstance of this.plugins) {
338
469
  if (bxoInstance.hooks.onResponse) {
339
- response = await bxoInstance.hooks.onResponse(ctx, response) || response;
470
+ response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
340
471
  }
341
472
  }
342
473
 
@@ -359,6 +490,35 @@ export default class BXO {
359
490
  return response;
360
491
  }
361
492
 
493
+ // Handle File response (like Elysia)
494
+ if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
495
+ const file = response as File;
496
+ const responseInit: ResponseInit = {
497
+ status: ctx.set.status || 200,
498
+ headers: {
499
+ 'Content-Type': file.type || 'application/octet-stream',
500
+ 'Content-Length': file.size.toString(),
501
+ ...ctx.set.headers
502
+ }
503
+ };
504
+ return new Response(file, responseInit);
505
+ }
506
+
507
+ // Handle Bun.file() response
508
+ if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
509
+ const bunFile = response as any;
510
+ const responseInit: ResponseInit = {
511
+ status: ctx.set.status || 200,
512
+ headers: {
513
+ 'Content-Type': bunFile.type || 'application/octet-stream',
514
+ 'Content-Length': bunFile.size?.toString() || '',
515
+ ...ctx.set.headers,
516
+ ...(bunFile.headers || {}) // Support custom headers from file helper
517
+ }
518
+ };
519
+ return new Response(bunFile, responseInit);
520
+ }
521
+
362
522
  const responseInit: ResponseInit = {
363
523
  status: ctx.set.status || 200,
364
524
  headers: ctx.set.headers || {}
@@ -381,12 +541,12 @@ export default class BXO {
381
541
  let errorResponse: any;
382
542
 
383
543
  if (this.hooks.onError) {
384
- errorResponse = await this.hooks.onError(ctx, error as Error);
544
+ errorResponse = await this.hooks.onError(ctx, error as Error, this);
385
545
  }
386
546
 
387
547
  for (const bxoInstance of this.plugins) {
388
548
  if (bxoInstance.hooks.onError) {
389
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
549
+ errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
390
550
  }
391
551
  }
392
552
 
@@ -409,77 +569,7 @@ export default class BXO {
409
569
  }
410
570
  }
411
571
 
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
572
 
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
-
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
573
 
484
574
  // Server management methods
485
575
  async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
@@ -491,27 +581,44 @@ export default class BXO {
491
581
  try {
492
582
  // Before start hook
493
583
  if (this.hooks.onBeforeStart) {
494
- await this.hooks.onBeforeStart();
584
+ await this.hooks.onBeforeStart(this);
495
585
  }
496
586
 
497
587
  this.server = Bun.serve({
498
588
  port,
499
589
  hostname,
500
- fetch: (request) => this.handleRequest(request),
590
+ fetch: (request, server) => this.handleRequest(request, server),
591
+ websocket: {
592
+ message: (ws: any, message: any) => {
593
+ const handler = ws.data?.handler;
594
+ if (handler?.onMessage) {
595
+ handler.onMessage(ws, message);
596
+ }
597
+ },
598
+ open: (ws: any) => {
599
+ const handler = ws.data?.handler;
600
+ if (handler?.onOpen) {
601
+ handler.onOpen(ws);
602
+ }
603
+ },
604
+ close: (ws: any, code?: number, reason?: string) => {
605
+ const handler = ws.data?.handler;
606
+ if (handler?.onClose) {
607
+ handler.onClose(ws, code, reason);
608
+ }
609
+ }
610
+ }
501
611
  });
502
612
 
503
613
  this.isRunning = true;
504
-
505
- console.log(`🦊 BXO server running at http://${hostname}:${port}`);
614
+ this.serverPort = port;
615
+ this.serverHostname = hostname;
506
616
 
507
617
  // After start hook
508
618
  if (this.hooks.onAfterStart) {
509
- await this.hooks.onAfterStart();
619
+ await this.hooks.onAfterStart(this);
510
620
  }
511
621
 
512
- // Setup hot reload
513
- await this.setupFileWatcher(port, hostname);
514
-
515
622
  // Handle graceful shutdown
516
623
  const shutdownHandler = async () => {
517
624
  await this.stop();
@@ -536,7 +643,7 @@ export default class BXO {
536
643
  try {
537
644
  // Before stop hook
538
645
  if (this.hooks.onBeforeStop) {
539
- await this.hooks.onBeforeStop();
646
+ await this.hooks.onBeforeStop(this);
540
647
  }
541
648
 
542
649
  if (this.server) {
@@ -545,12 +652,12 @@ export default class BXO {
545
652
  }
546
653
 
547
654
  this.isRunning = false;
548
-
549
- console.log('🛑 BXO server stopped');
655
+ this.serverPort = undefined;
656
+ this.serverHostname = undefined;
550
657
 
551
658
  // After stop hook
552
659
  if (this.hooks.onAfterStop) {
553
- await this.hooks.onAfterStop();
660
+ await this.hooks.onAfterStop(this);
554
661
  }
555
662
 
556
663
  } catch (error) {
@@ -559,32 +666,7 @@ export default class BXO {
559
666
  }
560
667
  }
561
668
 
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
-
571
- await this.stop();
572
-
573
- // Small delay to ensure cleanup
574
- await new Promise(resolve => setTimeout(resolve, 100));
575
669
 
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
670
 
589
671
  // Backward compatibility
590
672
  async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
@@ -596,22 +678,124 @@ export default class BXO {
596
678
  return this.isRunning;
597
679
  }
598
680
 
599
- getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
681
+ getServerInfo(): { running: boolean } {
600
682
  return {
683
+ running: this.isRunning
684
+ };
685
+ }
686
+
687
+ // Get server information (alias for getServerInfo)
688
+ get info() {
689
+ // Calculate total routes including plugins
690
+ const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
691
+ const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
692
+
693
+ return {
694
+ // Server status
601
695
  running: this.isRunning,
602
- hotReload: this.hotReloadEnabled,
603
- watchedFiles: Array.from(this.watchedFiles),
604
- excludePatterns: Array.from(this.watchedExclude)
696
+ server: this.server ? 'Bun' : null,
697
+
698
+ // Connection details
699
+ hostname: this.serverHostname,
700
+ port: this.serverPort,
701
+ url: this.isRunning && this.serverHostname && this.serverPort
702
+ ? `http://${this.serverHostname}:${this.serverPort}`
703
+ : null,
704
+
705
+ // Application statistics
706
+ totalRoutes,
707
+ totalWsRoutes,
708
+ totalPlugins: this.plugins.length,
709
+
710
+ // System information
711
+ runtime: 'Bun',
712
+ version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
713
+ pid: process.pid,
714
+ uptime: this.isRunning ? process.uptime() : 0
605
715
  };
606
716
  }
717
+
718
+ // Get all routes information
719
+ get routes() {
720
+ // Get routes from main instance
721
+ const mainRoutes = this._routes.map((route: Route) => ({
722
+ method: route.method,
723
+ path: route.path,
724
+ hasConfig: !!route.config,
725
+ config: route.config || null,
726
+ source: 'main' as const
727
+ }));
728
+
729
+ // Get routes from all plugins
730
+ const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
731
+ plugin._routes.map((route: Route) => ({
732
+ method: route.method,
733
+ path: route.path,
734
+ hasConfig: !!route.config,
735
+ config: route.config || null,
736
+ source: 'plugin' as const,
737
+ pluginIndex
738
+ }))
739
+ );
740
+
741
+ return [...mainRoutes, ...pluginRoutes];
742
+ }
743
+
744
+ // Get all WebSocket routes information
745
+ get wsRoutes() {
746
+ // Get WebSocket routes from main instance
747
+ const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
748
+ path: route.path,
749
+ hasHandlers: {
750
+ onOpen: !!route.handler.onOpen,
751
+ onMessage: !!route.handler.onMessage,
752
+ onClose: !!route.handler.onClose,
753
+ onError: !!route.handler.onError
754
+ },
755
+ source: 'main' as const
756
+ }));
757
+
758
+ // Get WebSocket routes from all plugins
759
+ const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
760
+ plugin._wsRoutes.map((route: WSRoute) => ({
761
+ path: route.path,
762
+ hasHandlers: {
763
+ onOpen: !!route.handler.onOpen,
764
+ onMessage: !!route.handler.onMessage,
765
+ onClose: !!route.handler.onClose,
766
+ onError: !!route.handler.onError
767
+ },
768
+ source: 'plugin' as const,
769
+ pluginIndex
770
+ }))
771
+ );
772
+
773
+ return [...mainWsRoutes, ...pluginWsRoutes];
774
+ }
775
+ }
776
+
777
+ const error = (error: Error | string, status: number = 500) => {
778
+ return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
607
779
  }
608
780
 
609
- const error = (error: Error, status: number = 500) => {
610
- return new Response(JSON.stringify({ error: error.message }), { status });
781
+ // File helper function (like Elysia)
782
+ const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
783
+ const bunFile = Bun.file(path);
784
+
785
+ if (options?.type) {
786
+ // Create a wrapper to override the MIME type
787
+ return {
788
+ ...bunFile,
789
+ type: options.type,
790
+ headers: options.headers
791
+ };
792
+ }
793
+
794
+ return bunFile;
611
795
  }
612
796
 
613
797
  // Export Zod for convenience
614
- export { z, error };
798
+ export { z, error, file };
615
799
 
616
800
  // Export types for external use
617
- export type { RouteConfig, Handler };
801
+ 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.11",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {