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