bxo 0.0.1 → 0.0.3

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
@@ -4,31 +4,46 @@ import { cors, logger, auth, rateLimit, createJWT } from './plugins';
4
4
  // Create the app instance
5
5
  const app = new BXO();
6
6
 
7
+ // Enable hot reload
8
+ app.enableHotReload(['./']); // Watch current directory
9
+
7
10
  // Add plugins
8
11
  app
9
12
  .use(logger({ format: 'simple' }))
10
- .use(cors({
13
+ .use(cors({
11
14
  origin: ['http://localhost:3000', 'https://example.com'],
12
- credentials: true
15
+ credentials: true
13
16
  }))
14
- .use(rateLimit({
15
- max: 100,
17
+ .use(rateLimit({
18
+ max: 100,
16
19
  window: 60, // 1 minute
17
- exclude: ['/health']
20
+ exclude: ['/health']
18
21
  }))
19
- .use(auth({
20
- type: 'jwt',
22
+ .use(auth({
23
+ type: 'jwt',
21
24
  secret: 'your-secret-key',
22
25
  exclude: ['/', '/login', '/health']
23
26
  }));
24
27
 
25
- // Add lifecycle hooks
28
+ // Add simplified lifecycle hooks
26
29
  app
27
- .onStart(() => {
28
- console.log('🚀 Server starting up...');
30
+ .onBeforeStart(() => {
31
+ console.log('🔧 Preparing to start server...');
32
+ })
33
+ .onAfterStart(() => {
34
+ console.log('✅ Server fully started and ready!');
35
+ })
36
+ .onBeforeStop(() => {
37
+ console.log('🔧 Preparing to stop server...');
38
+ })
39
+ .onAfterStop(() => {
40
+ console.log('✅ Server fully stopped!');
41
+ })
42
+ .onBeforeRestart(() => {
43
+ console.log('🔧 Preparing to restart server...');
29
44
  })
30
- .onStop(() => {
31
- console.log('🛑 Server shutting down...');
45
+ .onAfterRestart(() => {
46
+ console.log(' Server restart completed!');
32
47
  })
33
48
  .onRequest((ctx) => {
34
49
  console.log(`📨 Processing ${ctx.request.method} ${ctx.request.url}`);
@@ -48,7 +63,7 @@ app
48
63
  .get('/simple', async (ctx) => {
49
64
  return { message: 'Hello World' };
50
65
  })
51
-
66
+
52
67
  // Three arguments: path, handler, config
53
68
  .get('/users/:id', async (ctx) => {
54
69
  // ctx.params.id is fully typed as string (UUID)
@@ -58,7 +73,7 @@ app
58
73
  params: z.object({ id: z.string().uuid() }),
59
74
  query: z.object({ include: z.string().optional() })
60
75
  })
61
-
76
+
62
77
  .post('/users', async (ctx) => {
63
78
  // ctx.body is fully typed with name: string, email: string
64
79
  return { created: ctx.body };
@@ -71,18 +86,22 @@ app
71
86
 
72
87
  // Additional examples
73
88
  .get('/health', async (ctx) => {
74
- return { status: 'ok', timestamp: new Date().toISOString() };
89
+ return {
90
+ status: 'ok',
91
+ timestamp: new Date().toISOString(),
92
+ server: app.getServerInfo()
93
+ };
75
94
  })
76
95
 
77
96
  .post('/login', async (ctx) => {
78
97
  const { username, password } = ctx.body;
79
-
98
+
80
99
  // Simple auth check (in production, verify against database)
81
100
  if (username === 'admin' && password === 'password') {
82
101
  const token = createJWT({ username, role: 'admin' }, 'your-secret-key', 3600);
83
102
  return { token, user: { username, role: 'admin' } };
84
103
  }
85
-
104
+
86
105
  ctx.set.status = 401;
87
106
  return { error: 'Invalid credentials' };
88
107
  }, {
@@ -97,9 +116,24 @@ app
97
116
  return { message: 'This is protected', user: ctx.user };
98
117
  })
99
118
 
100
- .put('/users/:id', async (ctx) => {
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
+ .get('/status', async (ctx) => {
101
127
  return {
102
- updated: ctx.body,
128
+ ...app.getServerInfo(),
129
+ uptime: process.uptime(),
130
+ memory: process.memoryUsage()
131
+ };
132
+ })
133
+
134
+ .put('/users/:id', async (ctx) => {
135
+ return {
136
+ updated: ctx.body,
103
137
  id: ctx.params.id,
104
138
  version: ctx.headers['if-match']
105
139
  };
@@ -121,17 +155,29 @@ app
121
155
  params: z.object({ id: z.string().uuid() })
122
156
  });
123
157
 
124
- // Start the server
125
- app.listen(3000, 'localhost');
158
+ // Start the server (with hot reload enabled)
159
+ app.start(3000, 'localhost');
126
160
 
127
161
  console.log(`
128
- 🦊 BXO Framework Example
162
+ 🦊 BXO Framework with Hot Reload
129
163
 
130
- Try these endpoints:
164
+ Features Enabled:
165
+ - 🔄 Hot reload (edit any .ts/.js file to restart)
166
+ - 🎣 Full lifecycle hooks (before/after pattern)
167
+ - 🔒 JWT authentication
168
+ - 📊 Rate limiting
169
+ - 🌐 CORS support
170
+ - 📝 Request logging
171
+
172
+ 🧪 Try these endpoints:
131
173
  - GET /simple
132
174
  - GET /users/123e4567-e89b-12d3-a456-426614174000?include=profile
133
175
  - POST /users (with JSON body: {"name": "John", "email": "john@example.com"})
134
- - GET /health
176
+ - GET /health (shows server info)
135
177
  - POST /login (with JSON body: {"username": "admin", "password": "password"})
136
178
  - GET /protected (requires Bearer token from /login)
179
+ - GET /status (server statistics)
180
+ - POST /restart (restart server programmatically)
181
+
182
+ 💡 Edit this file and save to see hot reload in action!
137
183
  `);
package/index.ts CHANGED
@@ -30,13 +30,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
30
30
  // Handler function type
31
31
  type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
32
32
 
33
- // Plugin interface (also exported from plugins/index.ts)
34
- interface Plugin {
35
- name?: string;
36
- onRequest?: (ctx: Context) => Promise<void> | void;
37
- onResponse?: (ctx: Context, response: any) => Promise<any> | any;
38
- onError?: (ctx: Context, error: Error) => Promise<any> | any;
39
- }
33
+ // Note: Plugin interface moved to plugins/index.ts as PluginFactory type
40
34
 
41
35
 
42
36
 
@@ -50,8 +44,12 @@ interface Route {
50
44
 
51
45
  // Lifecycle hooks
52
46
  interface LifecycleHooks {
53
- onStart?: () => Promise<void> | void;
54
- onStop?: () => Promise<void> | void;
47
+ onBeforeStart?: () => Promise<void> | void;
48
+ onAfterStart?: () => Promise<void> | void;
49
+ onBeforeStop?: () => Promise<void> | void;
50
+ onAfterStop?: () => Promise<void> | void;
51
+ onBeforeRestart?: () => Promise<void> | void;
52
+ onAfterRestart?: () => Promise<void> | void;
55
53
  onRequest?: (ctx: Context) => Promise<void> | void;
56
54
  onResponse?: (ctx: Context, response: any) => Promise<any> | any;
57
55
  onError?: (ctx: Context, error: Error) => Promise<any> | any;
@@ -59,19 +57,43 @@ interface LifecycleHooks {
59
57
 
60
58
  export default class BXO {
61
59
  private routes: Route[] = [];
62
- private plugins: Plugin[] = [];
60
+ private plugins: BXO[] = [];
63
61
  private hooks: LifecycleHooks = {};
62
+ private server?: any;
63
+ private isRunning: boolean = false;
64
+ private hotReloadEnabled: boolean = false;
65
+ private watchedFiles: Set<string> = new Set();
64
66
 
65
- constructor() {}
67
+ constructor() { }
66
68
 
67
69
  // Lifecycle hook methods
68
- onStart(handler: () => Promise<void> | void): this {
69
- this.hooks.onStart = handler;
70
+ onBeforeStart(handler: () => Promise<void> | void): this {
71
+ this.hooks.onBeforeStart = handler;
72
+ return this;
73
+ }
74
+
75
+ onAfterStart(handler: () => Promise<void> | void): this {
76
+ this.hooks.onAfterStart = handler;
77
+ return this;
78
+ }
79
+
80
+ onBeforeStop(handler: () => Promise<void> | void): this {
81
+ this.hooks.onBeforeStop = handler;
70
82
  return this;
71
83
  }
72
84
 
73
- onStop(handler: () => Promise<void> | void): this {
74
- this.hooks.onStop = handler;
85
+ onAfterStop(handler: () => Promise<void> | void): this {
86
+ this.hooks.onAfterStop = handler;
87
+ return this;
88
+ }
89
+
90
+ onBeforeRestart(handler: () => Promise<void> | void): this {
91
+ this.hooks.onBeforeRestart = handler;
92
+ return this;
93
+ }
94
+
95
+ onAfterRestart(handler: () => Promise<void> | void): this {
96
+ this.hooks.onAfterRestart = handler;
75
97
  return this;
76
98
  }
77
99
 
@@ -90,9 +112,9 @@ export default class BXO {
90
112
  return this;
91
113
  }
92
114
 
93
- // Plugin system
94
- use(plugin: Plugin): this {
95
- this.plugins.push(plugin);
115
+ // Plugin system - now accepts other BXO instances
116
+ use(bxoInstance: BXO): this {
117
+ this.plugins.push(bxoInstance);
96
118
  return this;
97
119
  }
98
120
 
@@ -298,10 +320,10 @@ export default class BXO {
298
320
  await this.hooks.onRequest(ctx);
299
321
  }
300
322
 
301
- // Run plugin onRequest hooks
302
- for (const plugin of this.plugins) {
303
- if (plugin.onRequest) {
304
- await plugin.onRequest(ctx);
323
+ // Run BXO instance onRequest hooks
324
+ for (const bxoInstance of this.plugins) {
325
+ if (bxoInstance.hooks.onRequest) {
326
+ await bxoInstance.hooks.onRequest(ctx);
305
327
  }
306
328
  }
307
329
 
@@ -313,10 +335,10 @@ export default class BXO {
313
335
  response = await this.hooks.onResponse(ctx, response) || response;
314
336
  }
315
337
 
316
- // Run plugin onResponse hooks
317
- for (const plugin of this.plugins) {
318
- if (plugin.onResponse) {
319
- response = await plugin.onResponse(ctx, response) || response;
338
+ // Run BXO instance onResponse hooks
339
+ for (const bxoInstance of this.plugins) {
340
+ if (bxoInstance.hooks.onResponse) {
341
+ response = await bxoInstance.hooks.onResponse(ctx, response) || response;
320
342
  }
321
343
  }
322
344
 
@@ -350,9 +372,9 @@ export default class BXO {
350
372
  errorResponse = await this.hooks.onError(ctx, error as Error);
351
373
  }
352
374
 
353
- for (const plugin of this.plugins) {
354
- if (plugin.onError) {
355
- errorResponse = await plugin.onError(ctx, error as Error) || errorResponse;
375
+ for (const bxoInstance of this.plugins) {
376
+ if (bxoInstance.hooks.onError) {
377
+ errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
356
378
  }
357
379
  }
358
380
 
@@ -375,35 +397,165 @@ export default class BXO {
375
397
  }
376
398
  }
377
399
 
378
- // Start the server
379
- async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
380
- if (this.hooks.onStart) {
381
- await this.hooks.onStart();
400
+ // Hot reload functionality
401
+ enableHotReload(watchPaths: string[] = ['./']): this {
402
+ this.hotReloadEnabled = true;
403
+ watchPaths.forEach(path => this.watchedFiles.add(path));
404
+ return this;
405
+ }
406
+
407
+ private async setupFileWatcher(port: number, hostname: string): Promise<void> {
408
+ if (!this.hotReloadEnabled) return;
409
+
410
+ const fs = require('fs');
411
+
412
+ for (const watchPath of this.watchedFiles) {
413
+ try {
414
+ fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
415
+ if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
416
+ console.log(`🔄 File changed: ${filename}, restarting server...`);
417
+ await this.restart(port, hostname);
418
+ }
419
+ });
420
+ console.log(`👀 Watching ${watchPath} for changes...`);
421
+ } catch (error) {
422
+ console.warn(`⚠️ Could not watch ${watchPath}:`, error);
423
+ }
424
+ }
425
+ }
426
+
427
+ // Server management methods
428
+ async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
429
+ if (this.isRunning) {
430
+ console.log('⚠️ Server is already running');
431
+ return;
382
432
  }
383
433
 
384
- const server = Bun.serve({
385
- port,
386
- hostname,
387
- fetch: (request) => this.handleRequest(request),
388
- });
434
+ try {
435
+ // Before start hook
436
+ if (this.hooks.onBeforeStart) {
437
+ await this.hooks.onBeforeStart();
438
+ }
389
439
 
390
- console.log(`🦊 BXO server running at http://${hostname}:${port}`);
440
+ this.server = Bun.serve({
441
+ port,
442
+ hostname,
443
+ fetch: (request) => this.handleRequest(request),
444
+ });
391
445
 
392
- // Handle graceful shutdown
393
- process.on('SIGINT', async () => {
394
- if (this.hooks.onStop) {
395
- await this.hooks.onStop();
446
+ this.isRunning = true;
447
+
448
+ console.log(`🦊 BXO server running at http://${hostname}:${port}`);
449
+
450
+ // After start hook
451
+ if (this.hooks.onAfterStart) {
452
+ await this.hooks.onAfterStart();
396
453
  }
397
- server.stop();
398
- process.exit(0);
399
- });
454
+
455
+ // Setup hot reload
456
+ await this.setupFileWatcher(port, hostname);
457
+
458
+ // Handle graceful shutdown
459
+ const shutdownHandler = async () => {
460
+ await this.stop();
461
+ process.exit(0);
462
+ };
463
+
464
+ process.on('SIGINT', shutdownHandler);
465
+ process.on('SIGTERM', shutdownHandler);
466
+
467
+ } catch (error) {
468
+ console.error('❌ Failed to start server:', error);
469
+ throw error;
470
+ }
400
471
  }
472
+
473
+ async stop(): Promise<void> {
474
+ if (!this.isRunning) {
475
+ console.log('⚠️ Server is not running');
476
+ return;
477
+ }
478
+
479
+ try {
480
+ // Before stop hook
481
+ if (this.hooks.onBeforeStop) {
482
+ await this.hooks.onBeforeStop();
483
+ }
484
+
485
+ if (this.server) {
486
+ this.server.stop();
487
+ this.server = null;
488
+ }
489
+
490
+ this.isRunning = false;
491
+
492
+ console.log('🛑 BXO server stopped');
493
+
494
+ // After stop hook
495
+ if (this.hooks.onAfterStop) {
496
+ await this.hooks.onAfterStop();
497
+ }
498
+
499
+ } catch (error) {
500
+ console.error('❌ Error stopping server:', error);
501
+ throw error;
502
+ }
503
+ }
504
+
505
+ async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
506
+ try {
507
+ // Before restart hook
508
+ if (this.hooks.onBeforeRestart) {
509
+ await this.hooks.onBeforeRestart();
510
+ }
511
+
512
+ console.log('🔄 Restarting BXO server...');
513
+
514
+ await this.stop();
515
+
516
+ // Small delay to ensure cleanup
517
+ await new Promise(resolve => setTimeout(resolve, 100));
518
+
519
+ await this.start(port, hostname);
520
+
521
+ // After restart hook
522
+ if (this.hooks.onAfterRestart) {
523
+ await this.hooks.onAfterRestart();
524
+ }
525
+
526
+ } catch (error) {
527
+ console.error('❌ Error restarting server:', error);
528
+ throw error;
529
+ }
530
+ }
531
+
532
+ // Backward compatibility
533
+ async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
534
+ return this.start(port, hostname);
535
+ }
536
+
537
+ // Server status
538
+ isServerRunning(): boolean {
539
+ return this.isRunning;
540
+ }
541
+
542
+ getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[] } {
543
+ return {
544
+ running: this.isRunning,
545
+ hotReload: this.hotReloadEnabled,
546
+ watchedFiles: Array.from(this.watchedFiles)
547
+ };
548
+ }
549
+ }
550
+
551
+ const error = (error: Error, status: number = 500) => {
552
+ return new Response(JSON.stringify({ error: error.message }), { status });
401
553
  }
402
554
 
403
555
  // Export Zod for convenience
404
- export { z };
556
+ export { z, error };
405
557
 
406
- export type { Plugin } from './plugins';
558
+ export type { PluginFactory } from './plugins';
407
559
 
408
560
  // Export types for external use
409
561
  export type { RouteConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {
package/plugins/auth.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import BXO from '../index';
2
+
1
3
  interface AuthOptions {
2
4
  type: 'jwt' | 'bearer' | 'apikey';
3
5
  secret?: string;
@@ -6,7 +8,7 @@ interface AuthOptions {
6
8
  exclude?: string[];
7
9
  }
8
10
 
9
- export function auth(options: AuthOptions) {
11
+ export function auth(options: AuthOptions): BXO {
10
12
  const {
11
13
  type,
12
14
  secret,
@@ -15,84 +17,85 @@ export function auth(options: AuthOptions) {
15
17
  exclude = []
16
18
  } = options;
17
19
 
18
- return {
19
- name: 'auth',
20
- onRequest: async (ctx: any) => {
21
- const url = new URL(ctx.request.url);
22
- const pathname = url.pathname;
23
-
24
- // Skip auth for excluded paths
25
- if (exclude.some(path => {
26
- if (path.includes('*')) {
27
- const regex = new RegExp(path.replace(/\*/g, '.*'));
28
- return regex.test(pathname);
29
- }
30
- return pathname === path || pathname.startsWith(path);
31
- })) {
32
- return;
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);
33
31
  }
32
+ return pathname === path || pathname.startsWith(path);
33
+ })) {
34
+ return;
35
+ }
34
36
 
35
- const authHeader = ctx.request.headers.get(header.toLowerCase());
36
-
37
- if (!authHeader) {
38
- throw new Response(JSON.stringify({ error: 'Authorization header required' }), {
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>' }), {
39
51
  status: 401,
40
52
  headers: { 'Content-Type': 'application/json' }
41
53
  });
42
54
  }
55
+ token = authHeader.slice(7);
56
+ } else if (type === 'apikey') {
57
+ token = authHeader;
58
+ } else {
59
+ token = authHeader;
60
+ }
43
61
 
44
- let token: string;
62
+ try {
63
+ let user: any;
45
64
 
46
- if (type === 'jwt' || type === 'bearer') {
47
- if (!authHeader.startsWith('Bearer ')) {
48
- throw new Response(JSON.stringify({ error: 'Invalid authorization format. Use Bearer <token>' }), {
49
- status: 401,
50
- headers: { 'Content-Type': 'application/json' }
51
- });
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');
52
72
  }
53
- token = authHeader.slice(7);
54
- } else if (type === 'apikey') {
55
- token = authHeader;
56
- } else {
57
- token = authHeader;
58
- }
59
-
60
- try {
61
- let user: any;
62
73
 
63
- if (verify) {
64
- user = await verify(token, ctx);
65
- } else if (type === 'jwt' && secret) {
66
- // Simple JWT verification (in production, use a proper JWT library)
67
- const [headerB64, payloadB64, signature] = token.split('.');
68
- if (!headerB64 || !payloadB64 || !signature) {
69
- throw new Error('Invalid JWT format');
70
- }
71
-
72
- const payload = JSON.parse(atob(payloadB64));
73
-
74
- // Check expiration
75
- if (payload.exp && Date.now() >= payload.exp * 1000) {
76
- throw new Error('Token expired');
77
- }
78
-
79
- user = payload;
80
- } else {
81
- user = { token };
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');
82
79
  }
83
-
84
- // Attach user to context
85
- ctx.user = user;
86
80
 
87
- } catch (error) {
88
- const message = error instanceof Error ? error.message : 'Invalid token';
89
- throw new Response(JSON.stringify({ error: message }), {
90
- status: 401,
91
- headers: { 'Content-Type': 'application/json' }
92
- });
81
+ user = payload;
82
+ } else {
83
+ user = { token };
93
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
+ });
94
95
  }
95
- };
96
+ });
97
+
98
+ return authInstance;
96
99
  }
97
100
 
98
101
  // Helper function for creating JWT tokens (simple implementation)