bxo 0.0.1 → 0.0.2
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/README.md +570 -6
- package/example.ts +70 -24
- package/index.ts +177 -23
- package/package.json +1 -1
package/README.md
CHANGED
@@ -1,15 +1,579 @@
|
|
1
|
-
#
|
1
|
+
# 🦊 BXO - A Type-Safe Web Framework for Bun
|
2
2
|
|
3
|
-
|
3
|
+
BXO is a fast, lightweight, and fully type-safe web framework built specifically for Bun runtime. Inspired by Elysia, it provides excellent developer experience with automatic type inference, Zod validation, and a powerful plugin system.
|
4
|
+
|
5
|
+
## ✨ Features
|
6
|
+
|
7
|
+
- 🚀 **Built for Bun** - Leverages Bun's native HTTP server for maximum performance
|
8
|
+
- 🔒 **Type-Safe** - Full TypeScript support with automatic type inference
|
9
|
+
- 📋 **Zod Validation** - Built-in request/response validation using Zod schemas
|
10
|
+
- 🔌 **Plugin System** - Extensible architecture with lifecycle hooks
|
11
|
+
- 🎣 **Lifecycle Hooks** - Complete control over server and request/response lifecycle
|
12
|
+
- 🔄 **Hot Reload** - Automatic server restart on file changes during development
|
13
|
+
- 🎮 **Server Management** - Programmatic start, stop, and restart capabilities
|
14
|
+
- 📊 **Status Monitoring** - Built-in server status and runtime statistics
|
15
|
+
- 📦 **Zero Dependencies** - Only depends on Zod for validation
|
16
|
+
- ⚡ **Fast** - Minimal overhead with efficient routing
|
17
|
+
|
18
|
+
## 🚀 Quick Start
|
19
|
+
|
20
|
+
### Installation
|
21
|
+
|
22
|
+
```bash
|
23
|
+
bun add zod
|
24
|
+
```
|
25
|
+
|
26
|
+
### Basic Usage
|
27
|
+
|
28
|
+
```typescript
|
29
|
+
import BXO, { z } from './index';
|
30
|
+
|
31
|
+
const app = new BXO()
|
32
|
+
.get('/', async (ctx) => {
|
33
|
+
return { message: 'Hello, BXO!' };
|
34
|
+
})
|
35
|
+
.start(3000);
|
36
|
+
```
|
37
|
+
|
38
|
+
## 📚 Documentation
|
39
|
+
|
40
|
+
### HTTP Handlers
|
41
|
+
|
42
|
+
BXO supports all standard HTTP methods with fluent chaining:
|
43
|
+
|
44
|
+
```typescript
|
45
|
+
const app = new BXO()
|
46
|
+
// Simple handler
|
47
|
+
.get('/simple', async (ctx) => {
|
48
|
+
return { message: 'Hello World' };
|
49
|
+
})
|
50
|
+
|
51
|
+
// With validation
|
52
|
+
.post('/users', async (ctx) => {
|
53
|
+
// ctx.body is fully typed
|
54
|
+
return { created: ctx.body };
|
55
|
+
}, {
|
56
|
+
body: z.object({
|
57
|
+
name: z.string(),
|
58
|
+
email: z.string().email()
|
59
|
+
})
|
60
|
+
})
|
61
|
+
|
62
|
+
// Path parameters
|
63
|
+
.get('/users/:id', async (ctx) => {
|
64
|
+
// ctx.params.id is typed as UUID string
|
65
|
+
return { user: { id: ctx.params.id } };
|
66
|
+
}, {
|
67
|
+
params: z.object({
|
68
|
+
id: z.string().uuid()
|
69
|
+
}),
|
70
|
+
query: z.object({
|
71
|
+
include: z.string().optional()
|
72
|
+
})
|
73
|
+
})
|
74
|
+
|
75
|
+
// All HTTP methods supported
|
76
|
+
.put('/users/:id', handler)
|
77
|
+
.delete('/users/:id', handler)
|
78
|
+
.patch('/users/:id', handler);
|
79
|
+
```
|
80
|
+
|
81
|
+
### Context Object
|
82
|
+
|
83
|
+
The context object (`ctx`) provides access to request data and response configuration:
|
84
|
+
|
85
|
+
```typescript
|
86
|
+
interface Context<TConfig> {
|
87
|
+
params: InferredParamsType; // Path parameters
|
88
|
+
query: InferredQueryType; // Query string parameters
|
89
|
+
body: InferredBodyType; // Request body
|
90
|
+
headers: InferredHeadersType; // Request headers
|
91
|
+
request: Request; // Original Request object
|
92
|
+
set: { // Response configuration
|
93
|
+
status?: number;
|
94
|
+
headers?: Record<string, string>;
|
95
|
+
};
|
96
|
+
user?: any; // Added by auth plugin
|
97
|
+
[key: string]: any; // Extended by plugins
|
98
|
+
}
|
99
|
+
```
|
100
|
+
|
101
|
+
### Validation Configuration
|
102
|
+
|
103
|
+
Each route can specify validation schemas for different parts of the request:
|
104
|
+
|
105
|
+
```typescript
|
106
|
+
const config = {
|
107
|
+
params: z.object({ id: z.string().uuid() }),
|
108
|
+
query: z.object({
|
109
|
+
page: z.coerce.number().default(1),
|
110
|
+
limit: z.coerce.number().max(100).default(10)
|
111
|
+
}),
|
112
|
+
body: z.object({
|
113
|
+
name: z.string().min(1),
|
114
|
+
email: z.string().email()
|
115
|
+
}),
|
116
|
+
headers: z.object({
|
117
|
+
'content-type': z.literal('application/json')
|
118
|
+
})
|
119
|
+
};
|
120
|
+
|
121
|
+
app.post('/api/users/:id', async (ctx) => {
|
122
|
+
// All properties are fully typed based on schemas
|
123
|
+
const { id } = ctx.params; // string (UUID)
|
124
|
+
const { page, limit } = ctx.query; // number, number
|
125
|
+
const { name, email } = ctx.body; // string, string
|
126
|
+
}, config);
|
127
|
+
```
|
128
|
+
|
129
|
+
## 🔌 Plugin System
|
130
|
+
|
131
|
+
BXO has a powerful plugin system with lifecycle hooks. Plugins are separate modules imported from `./plugins`.
|
132
|
+
|
133
|
+
### Available Plugins
|
134
|
+
|
135
|
+
#### CORS Plugin
|
136
|
+
|
137
|
+
```typescript
|
138
|
+
import { cors } from './plugins';
|
139
|
+
|
140
|
+
app.use(cors({
|
141
|
+
origin: ['http://localhost:3000', 'https://example.com'],
|
142
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
143
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
144
|
+
credentials: true,
|
145
|
+
maxAge: 86400
|
146
|
+
}));
|
147
|
+
```
|
148
|
+
|
149
|
+
#### Logger Plugin
|
150
|
+
|
151
|
+
```typescript
|
152
|
+
import { logger } from './plugins';
|
153
|
+
|
154
|
+
app.use(logger({
|
155
|
+
format: 'simple', // 'simple' | 'detailed' | 'json'
|
156
|
+
includeBody: false, // Log request/response bodies
|
157
|
+
includeHeaders: false // Log headers
|
158
|
+
}));
|
159
|
+
```
|
160
|
+
|
161
|
+
#### Authentication Plugin
|
162
|
+
|
163
|
+
```typescript
|
164
|
+
import { auth, createJWT } from './plugins';
|
165
|
+
|
166
|
+
app.use(auth({
|
167
|
+
type: 'jwt', // 'jwt' | 'bearer' | 'apikey'
|
168
|
+
secret: 'your-secret-key',
|
169
|
+
exclude: ['/login', '/health'], // Skip auth for these paths
|
170
|
+
verify: async (token, ctx) => {
|
171
|
+
// Custom token verification
|
172
|
+
return { user: 'data' };
|
173
|
+
}
|
174
|
+
}));
|
175
|
+
|
176
|
+
// Create JWT tokens
|
177
|
+
const token = createJWT(
|
178
|
+
{ userId: 123, role: 'admin' },
|
179
|
+
'secret',
|
180
|
+
3600 // expires in 1 hour
|
181
|
+
);
|
182
|
+
```
|
183
|
+
|
184
|
+
#### Rate Limiting Plugin
|
185
|
+
|
186
|
+
```typescript
|
187
|
+
import { rateLimit } from './plugins';
|
188
|
+
|
189
|
+
app.use(rateLimit({
|
190
|
+
max: 100, // Max requests
|
191
|
+
window: 60, // Time window in seconds
|
192
|
+
exclude: ['/health'], // Skip rate limiting for these paths
|
193
|
+
keyGenerator: (ctx) => { // Custom key generation
|
194
|
+
return ctx.request.headers.get('x-api-key') || 'default';
|
195
|
+
},
|
196
|
+
message: 'Too many requests',
|
197
|
+
statusCode: 429
|
198
|
+
}));
|
199
|
+
```
|
200
|
+
|
201
|
+
### Creating Custom Plugins
|
202
|
+
|
203
|
+
```typescript
|
204
|
+
const customPlugin = {
|
205
|
+
name: 'custom',
|
206
|
+
onRequest: async (ctx) => {
|
207
|
+
console.log('Before request processing');
|
208
|
+
ctx.startTime = Date.now();
|
209
|
+
},
|
210
|
+
onResponse: async (ctx, response) => {
|
211
|
+
console.log(`Request took ${Date.now() - ctx.startTime}ms`);
|
212
|
+
return response;
|
213
|
+
},
|
214
|
+
onError: async (ctx, error) => {
|
215
|
+
console.error('Request failed:', error.message);
|
216
|
+
return { error: 'Custom error response' };
|
217
|
+
}
|
218
|
+
};
|
219
|
+
|
220
|
+
app.use(customPlugin);
|
221
|
+
```
|
222
|
+
|
223
|
+
## 🎣 Lifecycle Hooks
|
224
|
+
|
225
|
+
BXO provides comprehensive lifecycle hooks with a consistent before/after pattern for both server and request lifecycle:
|
226
|
+
|
227
|
+
### Server Lifecycle Hooks
|
228
|
+
|
229
|
+
```typescript
|
230
|
+
app
|
231
|
+
.onBeforeStart(() => {
|
232
|
+
console.log('🔧 Preparing to start server...');
|
233
|
+
})
|
234
|
+
.onAfterStart(() => {
|
235
|
+
console.log('✅ Server fully started and ready!');
|
236
|
+
})
|
237
|
+
.onBeforeStop(() => {
|
238
|
+
console.log('🔧 Preparing to stop server...');
|
239
|
+
})
|
240
|
+
.onAfterStop(() => {
|
241
|
+
console.log('✅ Server fully stopped!');
|
242
|
+
})
|
243
|
+
.onBeforeRestart(() => {
|
244
|
+
console.log('🔧 Preparing to restart server...');
|
245
|
+
})
|
246
|
+
.onAfterRestart(() => {
|
247
|
+
console.log('✅ Server restart completed!');
|
248
|
+
});
|
249
|
+
```
|
250
|
+
|
251
|
+
### Request Lifecycle Hooks
|
252
|
+
|
253
|
+
```typescript
|
254
|
+
app
|
255
|
+
.onRequest((ctx) => {
|
256
|
+
console.log(`📨 ${ctx.request.method} ${ctx.request.url}`);
|
257
|
+
})
|
258
|
+
.onResponse((ctx, response) => {
|
259
|
+
console.log(`📤 Response sent`);
|
260
|
+
return response; // Can modify response
|
261
|
+
})
|
262
|
+
.onError((ctx, error) => {
|
263
|
+
console.error(`💥 Error:`, error.message);
|
264
|
+
return { error: 'Something went wrong' }; // Can provide custom error response
|
265
|
+
});
|
266
|
+
```
|
267
|
+
|
268
|
+
## 🔄 Hot Reload & Server Management
|
269
|
+
|
270
|
+
BXO includes built-in hot reload and comprehensive server management capabilities:
|
271
|
+
|
272
|
+
### Hot Reload
|
273
|
+
|
274
|
+
```typescript
|
275
|
+
const app = new BXO();
|
276
|
+
|
277
|
+
// Enable hot reload - server will restart when files change
|
278
|
+
app.enableHotReload(['./']); // Watch current directory
|
279
|
+
|
280
|
+
// Hot reload will automatically restart the server when:
|
281
|
+
// - Any .ts or .js file changes in watched directories
|
282
|
+
// - Server lifecycle hooks are properly executed during restart
|
283
|
+
```
|
284
|
+
|
285
|
+
### Server Management
|
286
|
+
|
287
|
+
```typescript
|
288
|
+
const app = new BXO();
|
289
|
+
|
290
|
+
// Start server
|
291
|
+
await app.start(3000, 'localhost');
|
292
|
+
|
293
|
+
// Check if server is running
|
294
|
+
if (app.isServerRunning()) {
|
295
|
+
console.log('Server is running!');
|
296
|
+
}
|
297
|
+
|
298
|
+
// Get server information
|
299
|
+
const info = app.getServerInfo();
|
300
|
+
console.log(info); // { running: true, hotReload: true, watchedFiles: ['./'] }
|
301
|
+
|
302
|
+
// Restart server programmatically
|
303
|
+
await app.restart(3000, 'localhost');
|
304
|
+
|
305
|
+
// Stop server gracefully
|
306
|
+
await app.stop();
|
307
|
+
|
308
|
+
// Backward compatibility - listen() still works
|
309
|
+
await app.listen(3000); // Same as app.start(3000)
|
310
|
+
```
|
311
|
+
|
312
|
+
### Development vs Production
|
313
|
+
|
314
|
+
```typescript
|
315
|
+
const app = new BXO();
|
316
|
+
|
317
|
+
if (process.env.NODE_ENV === 'development') {
|
318
|
+
// Enable hot reload in development
|
319
|
+
app.enableHotReload(['./src', './routes']);
|
320
|
+
}
|
321
|
+
|
322
|
+
// Add server management endpoints for development
|
323
|
+
if (process.env.NODE_ENV === 'development') {
|
324
|
+
app.post('/dev/restart', async (ctx) => {
|
325
|
+
setTimeout(() => app.restart(3000), 100);
|
326
|
+
return { message: 'Server restart initiated' };
|
327
|
+
});
|
328
|
+
|
329
|
+
app.get('/dev/status', async (ctx) => {
|
330
|
+
return {
|
331
|
+
...app.getServerInfo(),
|
332
|
+
uptime: process.uptime(),
|
333
|
+
memory: process.memoryUsage()
|
334
|
+
};
|
335
|
+
});
|
336
|
+
}
|
337
|
+
```
|
338
|
+
|
339
|
+
## 🌟 Complete Example
|
340
|
+
|
341
|
+
```typescript
|
342
|
+
import BXO, { z } from './index';
|
343
|
+
import { cors, logger, auth, rateLimit, createJWT } from './plugins';
|
344
|
+
|
345
|
+
const app = new BXO();
|
346
|
+
|
347
|
+
// Enable hot reload for development
|
348
|
+
app.enableHotReload(['./']);
|
349
|
+
|
350
|
+
// Add plugins
|
351
|
+
app
|
352
|
+
.use(logger({ format: 'simple' }))
|
353
|
+
.use(cors({
|
354
|
+
origin: ['http://localhost:3000'],
|
355
|
+
credentials: true
|
356
|
+
}))
|
357
|
+
.use(rateLimit({
|
358
|
+
max: 100,
|
359
|
+
window: 60,
|
360
|
+
exclude: ['/health']
|
361
|
+
}))
|
362
|
+
.use(auth({
|
363
|
+
type: 'jwt',
|
364
|
+
secret: 'your-secret-key',
|
365
|
+
exclude: ['/', '/login', '/health']
|
366
|
+
}));
|
367
|
+
|
368
|
+
// Comprehensive lifecycle hooks
|
369
|
+
app
|
370
|
+
.onBeforeStart(() => console.log('🔧 Preparing server startup...'))
|
371
|
+
.onAfterStart(() => console.log('✅ Server ready!'))
|
372
|
+
.onBeforeRestart(() => console.log('🔄 Restarting server...'))
|
373
|
+
.onAfterRestart(() => console.log('✅ Server restarted!'))
|
374
|
+
.onError((ctx, error) => ({
|
375
|
+
error: 'Internal server error',
|
376
|
+
timestamp: new Date().toISOString()
|
377
|
+
}));
|
378
|
+
|
379
|
+
// Routes
|
380
|
+
app
|
381
|
+
.get('/health', async () => ({
|
382
|
+
status: 'ok',
|
383
|
+
timestamp: new Date().toISOString(),
|
384
|
+
server: app.getServerInfo()
|
385
|
+
}))
|
386
|
+
|
387
|
+
.post('/login', async (ctx) => {
|
388
|
+
const { username, password } = ctx.body;
|
389
|
+
|
390
|
+
if (username === 'admin' && password === 'password') {
|
391
|
+
const token = createJWT(
|
392
|
+
{ username, role: 'admin' },
|
393
|
+
'your-secret-key'
|
394
|
+
);
|
395
|
+
return { token, user: { username, role: 'admin' } };
|
396
|
+
}
|
397
|
+
|
398
|
+
ctx.set.status = 401;
|
399
|
+
return { error: 'Invalid credentials' };
|
400
|
+
}, {
|
401
|
+
body: z.object({
|
402
|
+
username: z.string(),
|
403
|
+
password: z.string()
|
404
|
+
})
|
405
|
+
})
|
406
|
+
|
407
|
+
.get('/users/:id', async (ctx) => {
|
408
|
+
return {
|
409
|
+
user: {
|
410
|
+
id: ctx.params.id,
|
411
|
+
include: ctx.query.include
|
412
|
+
}
|
413
|
+
};
|
414
|
+
}, {
|
415
|
+
params: z.object({ id: z.string().uuid() }),
|
416
|
+
query: z.object({ include: z.string().optional() })
|
417
|
+
})
|
418
|
+
|
419
|
+
.post('/users', async (ctx) => {
|
420
|
+
return { created: ctx.body };
|
421
|
+
}, {
|
422
|
+
body: z.object({
|
423
|
+
name: z.string(),
|
424
|
+
email: z.string().email()
|
425
|
+
})
|
426
|
+
})
|
427
|
+
|
428
|
+
.get('/protected', async (ctx) => {
|
429
|
+
// ctx.user available from auth plugin
|
430
|
+
return {
|
431
|
+
message: 'Protected resource',
|
432
|
+
user: ctx.user
|
433
|
+
};
|
434
|
+
})
|
435
|
+
|
436
|
+
// Server management endpoints
|
437
|
+
.post('/restart', async (ctx) => {
|
438
|
+
setTimeout(() => app.restart(3000), 100);
|
439
|
+
return { message: 'Server restart initiated' };
|
440
|
+
})
|
441
|
+
|
442
|
+
.get('/status', async (ctx) => {
|
443
|
+
return {
|
444
|
+
...app.getServerInfo(),
|
445
|
+
uptime: process.uptime(),
|
446
|
+
memory: process.memoryUsage()
|
447
|
+
};
|
448
|
+
});
|
449
|
+
|
450
|
+
// Start server with hot reload
|
451
|
+
app.start(3000);
|
452
|
+
```
|
453
|
+
|
454
|
+
## 🧪 Testing Endpoints
|
455
|
+
|
456
|
+
With the example server running, test these endpoints:
|
4
457
|
|
5
458
|
```bash
|
6
|
-
|
459
|
+
# Health check
|
460
|
+
curl http://localhost:3000/health
|
461
|
+
|
462
|
+
# Login to get token
|
463
|
+
curl -X POST http://localhost:3000/login \
|
464
|
+
-H "Content-Type: application/json" \
|
465
|
+
-d '{"username": "admin", "password": "password"}'
|
466
|
+
|
467
|
+
# Create user
|
468
|
+
curl -X POST http://localhost:3000/users \
|
469
|
+
-H "Content-Type: application/json" \
|
470
|
+
-d '{"name": "John Doe", "email": "john@example.com"}'
|
471
|
+
|
472
|
+
# Get user with validation
|
473
|
+
curl "http://localhost:3000/users/123e4567-e89b-12d3-a456-426614174000?include=profile"
|
474
|
+
|
475
|
+
# Access protected route (use token from login)
|
476
|
+
curl http://localhost:3000/protected \
|
477
|
+
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
478
|
+
|
479
|
+
# Check server status
|
480
|
+
curl http://localhost:3000/status
|
481
|
+
|
482
|
+
# Restart server programmatically
|
483
|
+
curl -X POST http://localhost:3000/restart
|
484
|
+
```
|
485
|
+
|
486
|
+
## 📖 API Reference
|
487
|
+
|
488
|
+
### BXO Class Methods
|
489
|
+
|
490
|
+
#### HTTP Methods
|
491
|
+
- `get(path, handler, config?)` - Handle GET requests
|
492
|
+
- `post(path, handler, config?)` - Handle POST requests
|
493
|
+
- `put(path, handler, config?)` - Handle PUT requests
|
494
|
+
- `delete(path, handler, config?)` - Handle DELETE requests
|
495
|
+
- `patch(path, handler, config?)` - Handle PATCH requests
|
496
|
+
|
497
|
+
#### Plugins & Hooks
|
498
|
+
- `use(plugin)` - Add a plugin
|
499
|
+
- `onBeforeStart(handler)` - Before server start hook
|
500
|
+
- `onAfterStart(handler)` - After server start hook
|
501
|
+
- `onBeforeStop(handler)` - Before server stop hook
|
502
|
+
- `onAfterStop(handler)` - After server stop hook
|
503
|
+
- `onBeforeRestart(handler)` - Before server restart hook
|
504
|
+
- `onAfterRestart(handler)` - After server restart hook
|
505
|
+
- `onRequest(handler)` - Global request hook
|
506
|
+
- `onResponse(handler)` - Global response hook
|
507
|
+
- `onError(handler)` - Global error hook
|
508
|
+
|
509
|
+
#### Server Management
|
510
|
+
- `start(port?, hostname?)` - Start the server
|
511
|
+
- `stop()` - Stop the server gracefully
|
512
|
+
- `restart(port?, hostname?)` - Restart the server
|
513
|
+
- `listen(port?, hostname?)` - Start the server (backward compatibility)
|
514
|
+
- `isServerRunning()` - Check if server is running
|
515
|
+
- `getServerInfo()` - Get server status information
|
516
|
+
|
517
|
+
#### Hot Reload
|
518
|
+
- `enableHotReload(watchPaths?)` - Enable hot reload with file watching
|
519
|
+
|
520
|
+
### Route Configuration
|
521
|
+
|
522
|
+
```typescript
|
523
|
+
interface RouteConfig {
|
524
|
+
params?: z.ZodSchema<any>; // Path parameter validation
|
525
|
+
query?: z.ZodSchema<any>; // Query string validation
|
526
|
+
body?: z.ZodSchema<any>; // Request body validation
|
527
|
+
headers?: z.ZodSchema<any>; // Header validation
|
528
|
+
}
|
7
529
|
```
|
8
530
|
|
9
|
-
|
531
|
+
### Plugin Interface
|
532
|
+
|
533
|
+
```typescript
|
534
|
+
interface Plugin {
|
535
|
+
name?: string;
|
536
|
+
onRequest?: (ctx: Context) => Promise<void> | void;
|
537
|
+
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
538
|
+
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
539
|
+
}
|
540
|
+
```
|
541
|
+
|
542
|
+
## 🛠️ Development
|
543
|
+
|
544
|
+
### Running the Example
|
10
545
|
|
11
546
|
```bash
|
12
|
-
|
547
|
+
# Run with hot reload enabled
|
548
|
+
bun run example.ts
|
549
|
+
|
550
|
+
# The server will automatically restart when you edit any .ts/.js files!
|
551
|
+
```
|
552
|
+
|
553
|
+
### Project Structure
|
554
|
+
|
555
|
+
```
|
556
|
+
bxo/
|
557
|
+
├── index.ts # Main BXO framework
|
558
|
+
├── plugins/
|
559
|
+
│ ├── index.ts # Plugin exports
|
560
|
+
│ ├── cors.ts # CORS plugin
|
561
|
+
│ ├── logger.ts # Logger plugin
|
562
|
+
│ ├── auth.ts # Authentication plugin
|
563
|
+
│ └── ratelimit.ts # Rate limiting plugin
|
564
|
+
├── example.ts # Usage example
|
565
|
+
├── package.json
|
566
|
+
└── README.md
|
13
567
|
```
|
14
568
|
|
15
|
-
|
569
|
+
## 🤝 Contributing
|
570
|
+
|
571
|
+
BXO is designed to be simple and extensible. Contributions are welcome!
|
572
|
+
|
573
|
+
## 📄 License
|
574
|
+
|
575
|
+
MIT License - feel free to use BXO in your projects!
|
576
|
+
|
577
|
+
---
|
578
|
+
|
579
|
+
Built with ❤️ for the Bun ecosystem
|
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
|
-
.
|
28
|
-
console.log('
|
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
|
-
.
|
31
|
-
console.log('
|
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 {
|
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
|
-
|
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
|
-
|
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.
|
158
|
+
// Start the server (with hot reload enabled)
|
159
|
+
app.start(3000, 'localhost');
|
126
160
|
|
127
161
|
console.log(`
|
128
|
-
🦊 BXO Framework
|
162
|
+
🦊 BXO Framework with Hot Reload
|
129
163
|
|
130
|
-
|
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
@@ -50,8 +50,12 @@ interface Route {
|
|
50
50
|
|
51
51
|
// Lifecycle hooks
|
52
52
|
interface LifecycleHooks {
|
53
|
-
|
54
|
-
|
53
|
+
onBeforeStart?: () => Promise<void> | void;
|
54
|
+
onAfterStart?: () => Promise<void> | void;
|
55
|
+
onBeforeStop?: () => Promise<void> | void;
|
56
|
+
onAfterStop?: () => Promise<void> | void;
|
57
|
+
onBeforeRestart?: () => Promise<void> | void;
|
58
|
+
onAfterRestart?: () => Promise<void> | void;
|
55
59
|
onRequest?: (ctx: Context) => Promise<void> | void;
|
56
60
|
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
57
61
|
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
@@ -61,17 +65,41 @@ export default class BXO {
|
|
61
65
|
private routes: Route[] = [];
|
62
66
|
private plugins: Plugin[] = [];
|
63
67
|
private hooks: LifecycleHooks = {};
|
68
|
+
private server?: any;
|
69
|
+
private isRunning: boolean = false;
|
70
|
+
private hotReloadEnabled: boolean = false;
|
71
|
+
private watchedFiles: Set<string> = new Set();
|
64
72
|
|
65
73
|
constructor() {}
|
66
74
|
|
67
75
|
// Lifecycle hook methods
|
68
|
-
|
69
|
-
this.hooks.
|
76
|
+
onBeforeStart(handler: () => Promise<void> | void): this {
|
77
|
+
this.hooks.onBeforeStart = handler;
|
70
78
|
return this;
|
71
79
|
}
|
72
80
|
|
73
|
-
|
74
|
-
this.hooks.
|
81
|
+
onAfterStart(handler: () => Promise<void> | void): this {
|
82
|
+
this.hooks.onAfterStart = handler;
|
83
|
+
return this;
|
84
|
+
}
|
85
|
+
|
86
|
+
onBeforeStop(handler: () => Promise<void> | void): this {
|
87
|
+
this.hooks.onBeforeStop = handler;
|
88
|
+
return this;
|
89
|
+
}
|
90
|
+
|
91
|
+
onAfterStop(handler: () => Promise<void> | void): this {
|
92
|
+
this.hooks.onAfterStop = handler;
|
93
|
+
return this;
|
94
|
+
}
|
95
|
+
|
96
|
+
onBeforeRestart(handler: () => Promise<void> | void): this {
|
97
|
+
this.hooks.onBeforeRestart = handler;
|
98
|
+
return this;
|
99
|
+
}
|
100
|
+
|
101
|
+
onAfterRestart(handler: () => Promise<void> | void): this {
|
102
|
+
this.hooks.onAfterRestart = handler;
|
75
103
|
return this;
|
76
104
|
}
|
77
105
|
|
@@ -375,28 +403,154 @@ export default class BXO {
|
|
375
403
|
}
|
376
404
|
}
|
377
405
|
|
378
|
-
//
|
379
|
-
|
380
|
-
|
381
|
-
|
406
|
+
// Hot reload functionality
|
407
|
+
enableHotReload(watchPaths: string[] = ['./']): this {
|
408
|
+
this.hotReloadEnabled = true;
|
409
|
+
watchPaths.forEach(path => this.watchedFiles.add(path));
|
410
|
+
return this;
|
411
|
+
}
|
412
|
+
|
413
|
+
private async setupFileWatcher(port: number, hostname: string): Promise<void> {
|
414
|
+
if (!this.hotReloadEnabled) return;
|
415
|
+
|
416
|
+
const fs = require('fs');
|
417
|
+
|
418
|
+
for (const watchPath of this.watchedFiles) {
|
419
|
+
try {
|
420
|
+
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
421
|
+
if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
|
422
|
+
console.log(`🔄 File changed: ${filename}, restarting server...`);
|
423
|
+
await this.restart(port, hostname);
|
424
|
+
}
|
425
|
+
});
|
426
|
+
console.log(`👀 Watching ${watchPath} for changes...`);
|
427
|
+
} catch (error) {
|
428
|
+
console.warn(`⚠️ Could not watch ${watchPath}:`, error);
|
429
|
+
}
|
430
|
+
}
|
431
|
+
}
|
432
|
+
|
433
|
+
// Server management methods
|
434
|
+
async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
435
|
+
if (this.isRunning) {
|
436
|
+
console.log('⚠️ Server is already running');
|
437
|
+
return;
|
438
|
+
}
|
439
|
+
|
440
|
+
try {
|
441
|
+
// Before start hook
|
442
|
+
if (this.hooks.onBeforeStart) {
|
443
|
+
await this.hooks.onBeforeStart();
|
444
|
+
}
|
445
|
+
|
446
|
+
this.server = Bun.serve({
|
447
|
+
port,
|
448
|
+
hostname,
|
449
|
+
fetch: (request) => this.handleRequest(request),
|
450
|
+
});
|
451
|
+
|
452
|
+
this.isRunning = true;
|
453
|
+
|
454
|
+
console.log(`🦊 BXO server running at http://${hostname}:${port}`);
|
455
|
+
|
456
|
+
// After start hook
|
457
|
+
if (this.hooks.onAfterStart) {
|
458
|
+
await this.hooks.onAfterStart();
|
459
|
+
}
|
460
|
+
|
461
|
+
// Setup hot reload
|
462
|
+
await this.setupFileWatcher(port, hostname);
|
463
|
+
|
464
|
+
// Handle graceful shutdown
|
465
|
+
const shutdownHandler = async () => {
|
466
|
+
await this.stop();
|
467
|
+
process.exit(0);
|
468
|
+
};
|
469
|
+
|
470
|
+
process.on('SIGINT', shutdownHandler);
|
471
|
+
process.on('SIGTERM', shutdownHandler);
|
472
|
+
|
473
|
+
} catch (error) {
|
474
|
+
console.error('❌ Failed to start server:', error);
|
475
|
+
throw error;
|
476
|
+
}
|
477
|
+
}
|
478
|
+
|
479
|
+
async stop(): Promise<void> {
|
480
|
+
if (!this.isRunning) {
|
481
|
+
console.log('⚠️ Server is not running');
|
482
|
+
return;
|
382
483
|
}
|
383
484
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
485
|
+
try {
|
486
|
+
// Before stop hook
|
487
|
+
if (this.hooks.onBeforeStop) {
|
488
|
+
await this.hooks.onBeforeStop();
|
489
|
+
}
|
490
|
+
|
491
|
+
if (this.server) {
|
492
|
+
this.server.stop();
|
493
|
+
this.server = null;
|
494
|
+
}
|
495
|
+
|
496
|
+
this.isRunning = false;
|
497
|
+
|
498
|
+
console.log('🛑 BXO server stopped');
|
499
|
+
|
500
|
+
// After stop hook
|
501
|
+
if (this.hooks.onAfterStop) {
|
502
|
+
await this.hooks.onAfterStop();
|
503
|
+
}
|
504
|
+
|
505
|
+
} catch (error) {
|
506
|
+
console.error('❌ Error stopping server:', error);
|
507
|
+
throw error;
|
508
|
+
}
|
509
|
+
}
|
389
510
|
|
390
|
-
|
511
|
+
async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
512
|
+
try {
|
513
|
+
// Before restart hook
|
514
|
+
if (this.hooks.onBeforeRestart) {
|
515
|
+
await this.hooks.onBeforeRestart();
|
516
|
+
}
|
391
517
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
518
|
+
console.log('🔄 Restarting BXO server...');
|
519
|
+
|
520
|
+
await this.stop();
|
521
|
+
|
522
|
+
// Small delay to ensure cleanup
|
523
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
524
|
+
|
525
|
+
await this.start(port, hostname);
|
526
|
+
|
527
|
+
// After restart hook
|
528
|
+
if (this.hooks.onAfterRestart) {
|
529
|
+
await this.hooks.onAfterRestart();
|
396
530
|
}
|
397
|
-
|
398
|
-
|
399
|
-
|
531
|
+
|
532
|
+
} catch (error) {
|
533
|
+
console.error('❌ Error restarting server:', error);
|
534
|
+
throw error;
|
535
|
+
}
|
536
|
+
}
|
537
|
+
|
538
|
+
// Backward compatibility
|
539
|
+
async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
540
|
+
return this.start(port, hostname);
|
541
|
+
}
|
542
|
+
|
543
|
+
// Server status
|
544
|
+
isServerRunning(): boolean {
|
545
|
+
return this.isRunning;
|
546
|
+
}
|
547
|
+
|
548
|
+
getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[] } {
|
549
|
+
return {
|
550
|
+
running: this.isRunning,
|
551
|
+
hotReload: this.hotReloadEnabled,
|
552
|
+
watchedFiles: Array.from(this.watchedFiles)
|
553
|
+
};
|
400
554
|
}
|
401
555
|
}
|
402
556
|
|