bxo 0.0.5-dev.2 โ†’ 0.0.5-dev.20

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/example.ts CHANGED
@@ -1,13 +1,39 @@
1
1
  import BXO, { z } from './index';
2
2
  import { cors, logger, auth, rateLimit, createJWT } from './plugins';
3
3
 
4
+ // Create a simple API plugin that defines its own routes
5
+ function createApiPlugin(): BXO {
6
+ const apiPlugin = new BXO();
7
+
8
+ apiPlugin
9
+ .get('/api/info', async (ctx) => {
10
+ return {
11
+ name: 'BXO API Plugin',
12
+ version: '1.0.0',
13
+ endpoints: ['/api/info', '/api/ping', '/api/time']
14
+ };
15
+ })
16
+ .get('/api/ping', async (ctx) => {
17
+ return { ping: 'pong', timestamp: Date.now() };
18
+ })
19
+ .get('/api/time', async (ctx) => {
20
+ return { time: new Date().toISOString() };
21
+ })
22
+ .post('/api/echo', async (ctx) => {
23
+ return { echo: ctx.body };
24
+ }, {
25
+ body: z.object({
26
+ message: z.string()
27
+ })
28
+ });
29
+
30
+ return apiPlugin;
31
+ }
32
+
4
33
  // Create the app instance
5
34
  const app = new BXO();
6
35
 
7
- // Enable hot reload
8
- app.enableHotReload(['./']); // Watch current directory
9
-
10
- // Add plugins
36
+ // Add plugins (including our new API plugin)
11
37
  app
12
38
  .use(logger({ format: 'simple' }))
13
39
  .use(cors({
@@ -22,8 +48,9 @@ app
22
48
  .use(auth({
23
49
  type: 'jwt',
24
50
  secret: 'your-secret-key',
25
- exclude: ['/', '/login', '/health']
26
- }));
51
+ exclude: ['/', '/login', '/health', '/api/*']
52
+ }))
53
+ .use(createApiPlugin()); // Add our plugin with actual routes
27
54
 
28
55
  // Add simplified lifecycle hooks
29
56
  app
@@ -39,12 +66,6 @@ app
39
66
  .onAfterStop(() => {
40
67
  console.log('โœ… Server fully stopped!');
41
68
  })
42
- .onBeforeRestart(() => {
43
- console.log('๐Ÿ”ง Preparing to restart server...');
44
- })
45
- .onAfterRestart(() => {
46
- console.log('โœ… Server restart completed!');
47
- })
48
69
  .onRequest((ctx) => {
49
70
  console.log(`๐Ÿ“จ Processing ${ctx.request.method} ${ctx.request.url}`);
50
71
  })
@@ -116,13 +137,6 @@ app
116
137
  return { message: 'This is protected', user: ctx.user };
117
138
  })
118
139
 
119
- // Server control endpoints
120
- .post('/restart', async (ctx) => {
121
- // Restart the server
122
- setTimeout(() => app.restart(3000), 100);
123
- return { message: 'Server restart initiated' };
124
- })
125
-
126
140
  .get('/status', async (ctx) => {
127
141
  return {
128
142
  ...app.getServerInfo(),
@@ -162,12 +176,12 @@ console.log(`
162
176
  ๐ŸฆŠ BXO Framework with Hot Reload
163
177
 
164
178
  โœจ Features Enabled:
165
- - ๐Ÿ”„ Hot reload (edit any .ts/.js file to restart)
166
179
  - ๐ŸŽฃ Full lifecycle hooks (before/after pattern)
167
180
  - ๐Ÿ”’ JWT authentication
168
181
  - ๐Ÿ“Š Rate limiting
169
182
  - ๐ŸŒ CORS support
170
183
  - ๐Ÿ“ Request logging
184
+ - ๐Ÿ”Œ API Plugin with routes
171
185
 
172
186
  ๐Ÿงช Try these endpoints:
173
187
  - GET /simple
@@ -177,7 +191,14 @@ console.log(`
177
191
  - POST /login (with JSON body: {"username": "admin", "password": "password"})
178
192
  - GET /protected (requires Bearer token from /login)
179
193
  - GET /status (server statistics)
180
- - POST /restart (restart server programmatically)
194
+
195
+ ๐Ÿ”Œ API Plugin endpoints:
196
+ - GET /api/info (plugin information)
197
+ - GET /api/ping (ping pong)
198
+ - GET /api/time (current time)
199
+ - POST /api/echo (echo message: {"message": "hello"})
181
200
 
182
201
  ๐Ÿ’ก Edit this file and save to see hot reload in action!
183
- `);
202
+ `);
203
+
204
+ console.log(app.routes)
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,18 +31,17 @@ 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;
24
38
  headers?: Record<string, string>;
25
39
  };
26
- // Extended properties that can be added by plugins
27
- user?: any;
28
40
  [key: string]: any;
29
41
  };
30
42
 
31
43
  // Handler function type
32
- type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
44
+ type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<any> | any;
33
45
 
34
46
  // Route definition
35
47
  interface Route {
@@ -39,73 +51,77 @@ interface Route {
39
51
  config?: RouteConfig;
40
52
  }
41
53
 
54
+ // WebSocket handler interface
55
+ interface WebSocketHandler {
56
+ onOpen?: (ws: any) => void;
57
+ onMessage?: (ws: any, message: string | Buffer) => void;
58
+ onClose?: (ws: any, code?: number, reason?: string) => void;
59
+ onError?: (ws: any, error: Error) => void;
60
+ }
61
+
62
+ // WebSocket route definition
63
+ interface WSRoute {
64
+ path: string;
65
+ handler: WebSocketHandler;
66
+ }
67
+
42
68
  // Lifecycle hooks
43
69
  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;
70
+ onBeforeStart?: (instance: BXO) => Promise<void> | void;
71
+ onAfterStart?: (instance: BXO) => Promise<void> | void;
72
+ onBeforeStop?: (instance: BXO) => Promise<void> | void;
73
+ onAfterStop?: (instance: BXO) => Promise<void> | void;
74
+ onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
75
+ onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
76
+ onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
53
77
  }
54
78
 
55
79
  export default class BXO {
56
80
  private _routes: Route[] = [];
81
+ private _wsRoutes: WSRoute[] = [];
57
82
  private plugins: BXO[] = [];
58
83
  private hooks: LifecycleHooks = {};
59
84
  private server?: any;
60
85
  private isRunning: boolean = false;
61
- private hotReloadEnabled: boolean = false;
62
- private watchedFiles: Set<string> = new Set();
63
- private watchedExclude: Set<string> = new Set();
86
+ private serverPort?: number;
87
+ private serverHostname?: string;
64
88
 
65
89
  constructor() { }
66
90
 
67
91
  // Lifecycle hook methods
68
- onBeforeStart(handler: () => Promise<void> | void): this {
92
+ onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
69
93
  this.hooks.onBeforeStart = handler;
70
94
  return this;
71
95
  }
72
96
 
73
- onAfterStart(handler: () => Promise<void> | void): this {
97
+ onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
74
98
  this.hooks.onAfterStart = handler;
75
99
  return this;
76
100
  }
77
101
 
78
- onBeforeStop(handler: () => Promise<void> | void): this {
102
+ onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
79
103
  this.hooks.onBeforeStop = handler;
80
104
  return this;
81
105
  }
82
106
 
83
- onAfterStop(handler: () => Promise<void> | void): this {
107
+ onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
84
108
  this.hooks.onAfterStop = handler;
85
109
  return this;
86
110
  }
87
111
 
88
- onBeforeRestart(handler: () => Promise<void> | void): this {
89
- this.hooks.onBeforeRestart = handler;
90
- return this;
91
- }
92
112
 
93
- onAfterRestart(handler: () => Promise<void> | void): this {
94
- this.hooks.onAfterRestart = handler;
95
- return this;
96
- }
97
113
 
98
- onRequest(handler: (ctx: Context) => Promise<void> | void): this {
114
+ onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
99
115
  this.hooks.onRequest = handler;
100
116
  return this;
101
117
  }
102
118
 
103
- onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
119
+ onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
104
120
  this.hooks.onResponse = handler;
105
121
  return this;
106
122
  }
107
123
 
108
- onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
124
+ onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
109
125
  this.hooks.onError = handler;
110
126
  return this;
111
127
  }
@@ -207,24 +223,135 @@ export default class BXO {
207
223
  return this;
208
224
  }
209
225
 
226
+ // WebSocket route handler
227
+ ws(path: string, handler: WebSocketHandler): this {
228
+ this._wsRoutes.push({ path, handler });
229
+ return this;
230
+ }
231
+
232
+ // Helper methods to get all routes including plugin routes
233
+ private getAllRoutes(): Route[] {
234
+ const allRoutes = [...this._routes];
235
+ for (const plugin of this.plugins) {
236
+ allRoutes.push(...plugin._routes);
237
+ }
238
+ return allRoutes;
239
+ }
240
+
241
+ private getAllWSRoutes(): WSRoute[] {
242
+ const allWSRoutes = [...this._wsRoutes];
243
+ for (const plugin of this.plugins) {
244
+ allWSRoutes.push(...plugin._wsRoutes);
245
+ }
246
+ return allWSRoutes;
247
+ }
248
+
210
249
  // Route matching utility
211
250
  private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
212
- for (const route of this._routes) {
251
+ const allRoutes = this.getAllRoutes();
252
+
253
+ for (const route of allRoutes) {
213
254
  if (route.method !== method) continue;
214
255
 
215
256
  const routeSegments = route.path.split('/').filter(Boolean);
216
257
  const pathSegments = pathname.split('/').filter(Boolean);
217
258
 
218
- if (routeSegments.length !== pathSegments.length) continue;
259
+ const params: Record<string, string> = {};
260
+ let isMatch = true;
261
+
262
+ // Handle wildcard at the end (catch-all)
263
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
264
+
265
+ if (hasWildcardAtEnd) {
266
+ // For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
267
+ if (pathSegments.length < routeSegments.length - 1) continue;
268
+ } else {
269
+ // For exact matching (with possible single-segment wildcards), lengths must match
270
+ if (routeSegments.length !== pathSegments.length) continue;
271
+ }
272
+
273
+ for (let i = 0; i < routeSegments.length; i++) {
274
+ const routeSegment = routeSegments[i];
275
+ const pathSegment = pathSegments[i];
276
+
277
+ if (!routeSegment) {
278
+ isMatch = false;
279
+ break;
280
+ }
281
+
282
+ // Handle catch-all wildcard at the end
283
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
284
+ // Wildcard at end matches remaining path segments
285
+ const remainingPath = pathSegments.slice(i).join('/');
286
+ params['*'] = remainingPath;
287
+ break;
288
+ }
289
+
290
+ if (!pathSegment) {
291
+ isMatch = false;
292
+ break;
293
+ }
294
+
295
+ if (routeSegment.startsWith(':')) {
296
+ const paramName = routeSegment.slice(1);
297
+ params[paramName] = decodeURIComponent(pathSegment);
298
+ } else if (routeSegment === '*') {
299
+ // Single segment wildcard
300
+ params['*'] = decodeURIComponent(pathSegment);
301
+ } else if (routeSegment !== pathSegment) {
302
+ isMatch = false;
303
+ break;
304
+ }
305
+ }
306
+
307
+ if (isMatch) {
308
+ return { route, params };
309
+ }
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ // WebSocket route matching utility
316
+ private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
317
+ const allWSRoutes = this.getAllWSRoutes();
318
+
319
+ for (const route of allWSRoutes) {
320
+ const routeSegments = route.path.split('/').filter(Boolean);
321
+ const pathSegments = pathname.split('/').filter(Boolean);
219
322
 
220
323
  const params: Record<string, string> = {};
221
324
  let isMatch = true;
222
325
 
326
+ // Handle wildcard at the end (catch-all)
327
+ const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
328
+
329
+ if (hasWildcardAtEnd) {
330
+ // For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
331
+ if (pathSegments.length < routeSegments.length - 1) continue;
332
+ } else {
333
+ // For exact matching (with possible single-segment wildcards), lengths must match
334
+ if (routeSegments.length !== pathSegments.length) continue;
335
+ }
336
+
223
337
  for (let i = 0; i < routeSegments.length; i++) {
224
338
  const routeSegment = routeSegments[i];
225
339
  const pathSegment = pathSegments[i];
226
340
 
227
- if (!routeSegment || !pathSegment) {
341
+ if (!routeSegment) {
342
+ isMatch = false;
343
+ break;
344
+ }
345
+
346
+ // Handle catch-all wildcard at the end
347
+ if (routeSegment === '*' && i === routeSegments.length - 1) {
348
+ // Wildcard at end matches remaining path segments
349
+ const remainingPath = pathSegments.slice(i).join('/');
350
+ params['*'] = remainingPath;
351
+ break;
352
+ }
353
+
354
+ if (!pathSegment) {
228
355
  isMatch = false;
229
356
  break;
230
357
  }
@@ -232,6 +359,9 @@ export default class BXO {
232
359
  if (routeSegment.startsWith(':')) {
233
360
  const paramName = routeSegment.slice(1);
234
361
  params[paramName] = decodeURIComponent(pathSegment);
362
+ } else if (routeSegment === '*') {
363
+ // Single segment wildcard
364
+ params['*'] = decodeURIComponent(pathSegment);
235
365
  } else if (routeSegment !== pathSegment) {
236
366
  isMatch = false;
237
367
  break;
@@ -271,11 +401,30 @@ export default class BXO {
271
401
  }
272
402
 
273
403
  // Main request handler
274
- private async handleRequest(request: Request): Promise<Response> {
404
+ private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
275
405
  const url = new URL(request.url);
276
406
  const method = request.method;
277
407
  const pathname = url.pathname;
278
408
 
409
+ // Check for WebSocket upgrade
410
+ if (request.headers.get('upgrade') === 'websocket') {
411
+ const wsMatchResult = this.matchWSRoute(pathname);
412
+ if (wsMatchResult && server) {
413
+ const success = server.upgrade(request, {
414
+ data: {
415
+ handler: wsMatchResult.route.handler,
416
+ params: wsMatchResult.params,
417
+ pathname
418
+ }
419
+ });
420
+
421
+ if (success) {
422
+ return; // undefined response means upgrade was successful
423
+ }
424
+ }
425
+ return new Response('WebSocket upgrade failed', { status: 400 });
426
+ }
427
+
279
428
  const matchResult = this.matchRoute(method, pathname);
280
429
  if (!matchResult) {
281
430
  return new Response('Not Found', { status: 404 });
@@ -298,30 +447,76 @@ export default class BXO {
298
447
  const formData = await request.formData();
299
448
  body = Object.fromEntries(formData.entries());
300
449
  } else {
301
- body = await request.text();
450
+ // Try to parse as JSON if it looks like JSON, otherwise treat as text
451
+ const textBody = await request.text();
452
+ try {
453
+ // Check if the text looks like JSON
454
+ if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
455
+ body = JSON.parse(textBody);
456
+ } else {
457
+ body = textBody;
458
+ }
459
+ } catch {
460
+ body = textBody;
461
+ }
302
462
  }
303
463
  }
304
464
 
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
- };
465
+ // Create context with validation
466
+ let ctx: Context;
467
+ try {
468
+ // Validate each part separately to get better error messages
469
+ const validatedParams = route.config?.params ? this.validateData(route.config.params, params) : params;
470
+ const validatedQuery = route.config?.query ? this.validateData(route.config.query, query) : query;
471
+ const validatedBody = route.config?.body ? this.validateData(route.config.body, body) : body;
472
+ const validatedHeaders = route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
473
+
474
+ ctx = {
475
+ params: validatedParams,
476
+ query: validatedQuery,
477
+ body: validatedBody,
478
+ headers: validatedHeaders,
479
+ path: pathname,
480
+ request,
481
+ set: {}
482
+ };
483
+ } catch (validationError) {
484
+ // Validation failed - return error response
485
+
486
+ // Extract detailed validation errors from Zod
487
+ let validationDetails = undefined;
488
+ if (validationError instanceof Error) {
489
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
490
+ validationDetails = validationError.errors;
491
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
492
+ validationDetails = validationError.issues;
493
+ }
494
+ }
495
+
496
+ // Create a clean error message
497
+ const errorMessage = validationDetails && validationDetails.length > 0
498
+ ? `Validation failed for ${validationDetails.length} field(s)`
499
+ : 'Validation failed';
500
+
501
+ return new Response(JSON.stringify({
502
+ error: errorMessage,
503
+ details: validationDetails
504
+ }), {
505
+ status: 400,
506
+ headers: { 'Content-Type': 'application/json' }
507
+ });
508
+ }
314
509
 
315
510
  try {
316
511
  // Run global onRequest hook
317
512
  if (this.hooks.onRequest) {
318
- await this.hooks.onRequest(ctx);
513
+ await this.hooks.onRequest(ctx, this);
319
514
  }
320
515
 
321
516
  // Run BXO instance onRequest hooks
322
517
  for (const bxoInstance of this.plugins) {
323
518
  if (bxoInstance.hooks.onRequest) {
324
- await bxoInstance.hooks.onRequest(ctx);
519
+ await bxoInstance.hooks.onRequest(ctx, this);
325
520
  }
326
521
  }
327
522
 
@@ -330,24 +525,43 @@ export default class BXO {
330
525
 
331
526
  // Run global onResponse hook
332
527
  if (this.hooks.onResponse) {
333
- response = await this.hooks.onResponse(ctx, response) || response;
528
+ response = await this.hooks.onResponse(ctx, response, this) || response;
334
529
  }
335
530
 
336
531
  // Run BXO instance onResponse hooks
337
532
  for (const bxoInstance of this.plugins) {
338
533
  if (bxoInstance.hooks.onResponse) {
339
- response = await bxoInstance.hooks.onResponse(ctx, response) || response;
534
+ response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
340
535
  }
341
536
  }
342
537
 
343
538
  // Validate response against schema if provided
344
539
  if (route.config?.response && !(response instanceof Response)) {
345
540
  try {
541
+ console.log('response', response);
346
542
  response = this.validateData(route.config.response, response);
347
543
  } catch (validationError) {
348
544
  // 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}` }), {
545
+
546
+ // Extract detailed validation errors from Zod
547
+ let validationDetails = undefined;
548
+ if (validationError instanceof Error) {
549
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
550
+ validationDetails = validationError.errors;
551
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
552
+ validationDetails = validationError.issues;
553
+ }
554
+ }
555
+
556
+ // Create a clean error message
557
+ const errorMessage = validationDetails && validationDetails.length > 0
558
+ ? `Response validation failed for ${validationDetails.length} field(s)`
559
+ : 'Response validation failed';
560
+
561
+ return new Response(JSON.stringify({
562
+ error: errorMessage,
563
+ details: validationDetails
564
+ }), {
351
565
  status: 500,
352
566
  headers: { 'Content-Type': 'application/json' }
353
567
  });
@@ -359,6 +573,35 @@ export default class BXO {
359
573
  return response;
360
574
  }
361
575
 
576
+ // Handle File response (like Elysia)
577
+ if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
578
+ const file = response as File;
579
+ const responseInit: ResponseInit = {
580
+ status: ctx.set.status || 200,
581
+ headers: {
582
+ 'Content-Type': file.type || 'application/octet-stream',
583
+ 'Content-Length': file.size.toString(),
584
+ ...ctx.set.headers
585
+ }
586
+ };
587
+ return new Response(file, responseInit);
588
+ }
589
+
590
+ // Handle Bun.file() response
591
+ if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
592
+ const bunFile = response as any;
593
+ const responseInit: ResponseInit = {
594
+ status: ctx.set.status || 200,
595
+ headers: {
596
+ 'Content-Type': bunFile.type || 'application/octet-stream',
597
+ 'Content-Length': bunFile.size?.toString() || '',
598
+ ...ctx.set.headers,
599
+ ...(bunFile.headers || {}) // Support custom headers from file helper
600
+ }
601
+ };
602
+ return new Response(bunFile, responseInit);
603
+ }
604
+
362
605
  const responseInit: ResponseInit = {
363
606
  status: ctx.set.status || 200,
364
607
  headers: ctx.set.headers || {}
@@ -381,12 +624,12 @@ export default class BXO {
381
624
  let errorResponse: any;
382
625
 
383
626
  if (this.hooks.onError) {
384
- errorResponse = await this.hooks.onError(ctx, error as Error);
627
+ errorResponse = await this.hooks.onError(ctx, error as Error, this);
385
628
  }
386
629
 
387
630
  for (const bxoInstance of this.plugins) {
388
631
  if (bxoInstance.hooks.onError) {
389
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
632
+ errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
390
633
  }
391
634
  }
392
635
 
@@ -409,77 +652,7 @@ export default class BXO {
409
652
  }
410
653
  }
411
654
 
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
655
 
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
656
 
484
657
  // Server management methods
485
658
  async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
@@ -491,27 +664,49 @@ 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
- this.isRunning = true;
696
+ // Verify server was created successfully
697
+ if (!this.server) {
698
+ throw new Error('Failed to create server instance');
699
+ }
504
700
 
505
- console.log(`๐ŸฆŠ BXO server running at http://${hostname}:${port}`);
701
+ this.isRunning = true;
702
+ this.serverPort = port;
703
+ this.serverHostname = hostname;
506
704
 
507
705
  // After start hook
508
706
  if (this.hooks.onAfterStart) {
509
- await this.hooks.onAfterStart();
707
+ await this.hooks.onAfterStart(this);
510
708
  }
511
709
 
512
- // Setup hot reload
513
- await this.setupFileWatcher(port, hostname);
514
-
515
710
  // Handle graceful shutdown
516
711
  const shutdownHandler = async () => {
517
712
  await this.stop();
@@ -536,55 +731,49 @@ 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) {
543
- this.server.stop();
544
- this.server = null;
738
+ try {
739
+ // Try to stop the server gracefully
740
+ if (typeof this.server.stop === 'function') {
741
+ this.server.stop();
742
+ } else {
743
+ console.warn('โš ๏ธ Server stop method not available');
744
+ }
745
+ } catch (stopError) {
746
+ console.error('โŒ Error calling server.stop():', stopError);
747
+ }
748
+
749
+ // Clear the server reference
750
+ this.server = undefined;
545
751
  }
546
752
 
753
+ // Reset state regardless of server.stop() success
547
754
  this.isRunning = false;
548
-
549
- console.log('๐Ÿ›‘ BXO server stopped');
755
+ this.serverPort = undefined;
756
+ this.serverHostname = undefined;
550
757
 
551
758
  // After stop hook
552
759
  if (this.hooks.onAfterStop) {
553
- await this.hooks.onAfterStop();
760
+ await this.hooks.onAfterStop(this);
554
761
  }
555
762
 
763
+ console.log('โœ… Server stopped successfully');
764
+
556
765
  } catch (error) {
557
766
  console.error('โŒ Error stopping server:', error);
767
+ // Even if there's an error, reset the state
768
+ this.isRunning = false;
769
+ this.server = undefined;
770
+ this.serverPort = undefined;
771
+ this.serverHostname = undefined;
558
772
  throw error;
559
773
  }
560
774
  }
561
775
 
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
776
 
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
777
 
589
778
  // Backward compatibility
590
779
  async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
@@ -593,51 +782,127 @@ export default class BXO {
593
782
 
594
783
  // Server status
595
784
  isServerRunning(): boolean {
596
- return this.isRunning;
785
+ return this.isRunning && this.server !== undefined;
597
786
  }
598
787
 
599
- getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
788
+ getServerInfo(): { running: boolean } {
600
789
  return {
601
- running: this.isRunning,
602
- hotReload: this.hotReloadEnabled,
603
- watchedFiles: Array.from(this.watchedFiles),
604
- excludePatterns: Array.from(this.watchedExclude)
790
+ running: this.isRunning
605
791
  };
606
792
  }
607
793
 
608
794
  // Get server information (alias for getServerInfo)
609
795
  get info() {
796
+ // Calculate total routes including plugins
797
+ const totalRoutes = this._routes.length + this.plugins.reduce((total, plugin) => total + plugin._routes.length, 0);
798
+ const totalWsRoutes = this._wsRoutes.length + this.plugins.reduce((total, plugin) => total + plugin._wsRoutes.length, 0);
799
+
610
800
  return {
611
- ...this.getServerInfo(),
612
- totalRoutes: this._routes.length,
801
+ // Server status
802
+ running: this.isRunning,
803
+ server: this.server ? 'Bun' : null,
804
+
805
+ // Connection details
806
+ hostname: this.serverHostname,
807
+ port: this.serverPort,
808
+ url: this.isRunning && this.serverHostname && this.serverPort
809
+ ? `http://${this.serverHostname}:${this.serverPort}`
810
+ : null,
811
+
812
+ // Application statistics
813
+ totalRoutes,
814
+ totalWsRoutes,
613
815
  totalPlugins: this.plugins.length,
614
- server: this.server ? 'Bun' : null
816
+
817
+ // System information
818
+ runtime: 'Bun',
819
+ version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
820
+ pid: process.pid,
821
+ uptime: this.isRunning ? process.uptime() : 0
615
822
  };
616
823
  }
617
824
 
618
825
  // Get all routes information
619
826
  get routes() {
620
- return this._routes.map((route: Route) => ({
827
+ // Get routes from main instance
828
+ const mainRoutes = this._routes.map((route: Route) => ({
621
829
  method: route.method,
622
830
  path: route.path,
623
831
  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
832
+ config: route.config || null,
833
+ source: 'main' as const
631
834
  }));
835
+
836
+ // Get routes from all plugins
837
+ const pluginRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
838
+ plugin._routes.map((route: Route) => ({
839
+ method: route.method,
840
+ path: route.path,
841
+ hasConfig: !!route.config,
842
+ config: route.config || null,
843
+ source: 'plugin' as const,
844
+ pluginIndex
845
+ }))
846
+ );
847
+
848
+ return [...mainRoutes, ...pluginRoutes];
632
849
  }
850
+
851
+ // Get all WebSocket routes information
852
+ get wsRoutes() {
853
+ // Get WebSocket routes from main instance
854
+ const mainWsRoutes = this._wsRoutes.map((route: WSRoute) => ({
855
+ path: route.path,
856
+ hasHandlers: {
857
+ onOpen: !!route.handler.onOpen,
858
+ onMessage: !!route.handler.onMessage,
859
+ onClose: !!route.handler.onClose,
860
+ onError: !!route.handler.onError
861
+ },
862
+ source: 'main' as const
863
+ }));
864
+
865
+ // Get WebSocket routes from all plugins
866
+ const pluginWsRoutes = this.plugins.flatMap((plugin, pluginIndex) =>
867
+ plugin._wsRoutes.map((route: WSRoute) => ({
868
+ path: route.path,
869
+ hasHandlers: {
870
+ onOpen: !!route.handler.onOpen,
871
+ onMessage: !!route.handler.onMessage,
872
+ onClose: !!route.handler.onClose,
873
+ onError: !!route.handler.onError
874
+ },
875
+ source: 'plugin' as const,
876
+ pluginIndex
877
+ }))
878
+ );
879
+
880
+ return [...mainWsRoutes, ...pluginWsRoutes];
881
+ }
882
+ }
883
+
884
+ const error = (error: Error | string, status: number = 500) => {
885
+ return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
633
886
  }
634
887
 
635
- const error = (error: Error, status: number = 500) => {
636
- return new Response(JSON.stringify({ error: error.message }), { status });
888
+ // File helper function (like Elysia)
889
+ const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
890
+ const bunFile = Bun.file(path);
891
+
892
+ if (options?.type) {
893
+ // Create a wrapper to override the MIME type
894
+ return {
895
+ ...bunFile,
896
+ type: options.type,
897
+ headers: options.headers
898
+ };
899
+ }
900
+
901
+ return bunFile;
637
902
  }
638
903
 
639
904
  // Export Zod for convenience
640
- export { z, error };
905
+ export { z, error, file };
641
906
 
642
907
  // Export types for external use
643
- export type { RouteConfig, Handler };
908
+ 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.2",
4
+ "version": "0.0.5-dev.20",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {
package/plugins/auth.ts DELETED
@@ -1,119 +0,0 @@
1
- import BXO from '../index';
2
-
3
- interface AuthOptions {
4
- type: 'jwt' | 'bearer' | 'apikey';
5
- secret?: string;
6
- header?: string;
7
- verify?: (token: string, ctx: any) => Promise<any> | any;
8
- exclude?: string[];
9
- }
10
-
11
- export function auth(options: AuthOptions): BXO {
12
- const {
13
- type,
14
- secret,
15
- header = 'authorization',
16
- verify,
17
- exclude = []
18
- } = options;
19
-
20
- const authInstance = new BXO();
21
-
22
- authInstance.onRequest(async (ctx: any) => {
23
- const url = new URL(ctx.request.url);
24
- const pathname = url.pathname;
25
-
26
- // Skip auth for excluded paths
27
- if (exclude.some(path => {
28
- if (path.includes('*')) {
29
- const regex = new RegExp(path.replace(/\*/g, '.*'));
30
- return regex.test(pathname);
31
- }
32
- return pathname === path || pathname.startsWith(path);
33
- })) {
34
- return;
35
- }
36
-
37
- const authHeader = ctx.request.headers.get(header.toLowerCase());
38
-
39
- if (!authHeader) {
40
- throw new Response(JSON.stringify({ error: 'Authorization header required' }), {
41
- status: 401,
42
- headers: { 'Content-Type': 'application/json' }
43
- });
44
- }
45
-
46
- let token: string;
47
-
48
- if (type === 'jwt' || type === 'bearer') {
49
- if (!authHeader.startsWith('Bearer ')) {
50
- throw new Response(JSON.stringify({ error: 'Invalid authorization format. Use Bearer <token>' }), {
51
- status: 401,
52
- headers: { 'Content-Type': 'application/json' }
53
- });
54
- }
55
- token = authHeader.slice(7);
56
- } else if (type === 'apikey') {
57
- token = authHeader;
58
- } else {
59
- token = authHeader;
60
- }
61
-
62
- try {
63
- let user: any;
64
-
65
- if (verify) {
66
- user = await verify(token, ctx);
67
- } else if (type === 'jwt' && secret) {
68
- // Simple JWT verification (in production, use a proper JWT library)
69
- const [headerB64, payloadB64, signature] = token.split('.');
70
- if (!headerB64 || !payloadB64 || !signature) {
71
- throw new Error('Invalid JWT format');
72
- }
73
-
74
- const payload = JSON.parse(atob(payloadB64));
75
-
76
- // Check expiration
77
- if (payload.exp && Date.now() >= payload.exp * 1000) {
78
- throw new Error('Token expired');
79
- }
80
-
81
- user = payload;
82
- } else {
83
- user = { token };
84
- }
85
-
86
- // Attach user to context
87
- ctx.user = user;
88
-
89
- } catch (error) {
90
- const message = error instanceof Error ? error.message : 'Invalid token';
91
- throw new Response(JSON.stringify({ error: message }), {
92
- status: 401,
93
- headers: { 'Content-Type': 'application/json' }
94
- });
95
- }
96
- });
97
-
98
- return authInstance;
99
- }
100
-
101
- // Helper function for creating JWT tokens (simple implementation)
102
- export function createJWT(payload: any, secret: string, expiresIn: number = 3600): string {
103
- const header = { alg: 'HS256', typ: 'JWT' };
104
- const now = Math.floor(Date.now() / 1000);
105
-
106
- const jwtPayload = {
107
- ...payload,
108
- iat: now,
109
- exp: now + expiresIn
110
- };
111
-
112
- const headerB64 = btoa(JSON.stringify(header));
113
- const payloadB64 = btoa(JSON.stringify(jwtPayload));
114
-
115
- // Simple signature (in production, use proper HMAC-SHA256)
116
- const signature = btoa(`${headerB64}.${payloadB64}.${secret}`);
117
-
118
- return `${headerB64}.${payloadB64}.${signature}`;
119
- }
package/plugins/logger.ts DELETED
@@ -1,109 +0,0 @@
1
- import BXO from '../index';
2
-
3
- interface LoggerOptions {
4
- format?: 'simple' | 'detailed' | 'json';
5
- includeBody?: boolean;
6
- includeHeaders?: boolean;
7
- }
8
-
9
- export function logger(options: LoggerOptions = {}): BXO {
10
- const {
11
- format = 'simple',
12
- includeBody = false,
13
- includeHeaders = false
14
- } = options;
15
-
16
- const loggerInstance = new BXO();
17
-
18
- loggerInstance.onRequest(async (ctx: any) => {
19
- ctx._startTime = Date.now();
20
-
21
- if (format === 'json') {
22
- const logData: any = {
23
- timestamp: new Date().toISOString(),
24
- method: ctx.request.method,
25
- url: ctx.request.url,
26
- type: 'request'
27
- };
28
-
29
- if (includeHeaders) {
30
- logData.headers = Object.fromEntries(ctx.request.headers.entries());
31
- }
32
-
33
- if (includeBody && ctx.body) {
34
- logData.body = ctx.body;
35
- }
36
-
37
- console.log(JSON.stringify(logData));
38
- } else if (format === 'detailed') {
39
- console.log(`โ†’ ${ctx.request.method} ${ctx.request.url}`);
40
- if (includeHeaders) {
41
- console.log(' Headers:', Object.fromEntries(ctx.request.headers.entries()));
42
- }
43
- if (includeBody && ctx.body) {
44
- console.log(' Body:', ctx.body);
45
- }
46
- } else {
47
- console.log(`โ†’ ${ctx.request.method} ${ctx.request.url}`);
48
- }
49
- });
50
-
51
- loggerInstance.onResponse(async (ctx: any, response: any) => {
52
- const duration = Date.now() - (ctx._startTime || 0);
53
- const status = ctx.set.status || 200;
54
-
55
- if (format === 'json') {
56
- const logData: any = {
57
- timestamp: new Date().toISOString(),
58
- method: ctx.request.method,
59
- url: ctx.request.url,
60
- status,
61
- duration: `${duration}ms`,
62
- type: 'response'
63
- };
64
-
65
- if (includeHeaders && ctx.set.headers) {
66
- logData.responseHeaders = ctx.set.headers;
67
- }
68
-
69
- if (includeBody && response) {
70
- logData.response = response;
71
- }
72
-
73
- console.log(JSON.stringify(logData));
74
- } else if (format === 'detailed') {
75
- console.log(`โ† ${ctx.request.method} ${ctx.request.url} ${status} ${duration}ms`);
76
- if (includeHeaders && ctx.set.headers) {
77
- console.log(' Response Headers:', ctx.set.headers);
78
- }
79
- if (includeBody && response) {
80
- console.log(' Response:', response);
81
- }
82
- } else {
83
- const statusColor = status >= 400 ? '\x1b[31m' : status >= 300 ? '\x1b[33m' : '\x1b[32m';
84
- const resetColor = '\x1b[0m';
85
- console.log(`โ† ${ctx.request.method} ${ctx.request.url} ${statusColor}${status}${resetColor} ${duration}ms`);
86
- }
87
-
88
- return response;
89
- });
90
-
91
- loggerInstance.onError(async (ctx: any, error: Error) => {
92
- const duration = Date.now() - (ctx._startTime || 0);
93
-
94
- if (format === 'json') {
95
- console.log(JSON.stringify({
96
- timestamp: new Date().toISOString(),
97
- method: ctx.request.method,
98
- url: ctx.request.url,
99
- error: error.message,
100
- duration: `${duration}ms`,
101
- type: 'error'
102
- }));
103
- } else {
104
- console.log(`โœ— ${ctx.request.method} ${ctx.request.url} \x1b[31mERROR\x1b[0m ${duration}ms: ${error.message}`);
105
- }
106
- });
107
-
108
- return loggerInstance;
109
- }