bxo 0.0.5-dev.1 → 0.0.5-dev.10
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/index.ts +319 -51
- package/package.json +1 -1
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,21 +53,36 @@ interface Route {
|
|
39
53
|
config?: RouteConfig;
|
40
54
|
}
|
41
55
|
|
56
|
+
// WebSocket handler interface
|
57
|
+
interface WebSocketHandler {
|
58
|
+
onOpen?: (ws: any) => void;
|
59
|
+
onMessage?: (ws: any, message: string | Buffer) => void;
|
60
|
+
onClose?: (ws: any, code?: number, reason?: string) => void;
|
61
|
+
onError?: (ws: any, error: Error) => void;
|
62
|
+
}
|
63
|
+
|
64
|
+
// WebSocket route definition
|
65
|
+
interface WSRoute {
|
66
|
+
path: string;
|
67
|
+
handler: WebSocketHandler;
|
68
|
+
}
|
69
|
+
|
42
70
|
// Lifecycle hooks
|
43
71
|
interface LifecycleHooks {
|
44
|
-
onBeforeStart?: () => Promise<void> | void;
|
45
|
-
onAfterStart?: () => Promise<void> | void;
|
46
|
-
onBeforeStop?: () => Promise<void> | void;
|
47
|
-
onAfterStop?: () => Promise<void> | void;
|
48
|
-
onBeforeRestart?: () => Promise<void> | void;
|
49
|
-
onAfterRestart?: () => Promise<void> | void;
|
50
|
-
onRequest?: (ctx: Context) => Promise<void> | void;
|
51
|
-
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
52
|
-
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
72
|
+
onBeforeStart?: (instance: BXO) => Promise<void> | void;
|
73
|
+
onAfterStart?: (instance: BXO) => Promise<void> | void;
|
74
|
+
onBeforeStop?: (instance: BXO) => Promise<void> | void;
|
75
|
+
onAfterStop?: (instance: BXO) => Promise<void> | void;
|
76
|
+
onBeforeRestart?: (instance: BXO) => Promise<void> | void;
|
77
|
+
onAfterRestart?: (instance: BXO) => Promise<void> | void;
|
78
|
+
onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
|
79
|
+
onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
|
80
|
+
onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
|
53
81
|
}
|
54
82
|
|
55
83
|
export default class BXO {
|
56
|
-
private
|
84
|
+
private _routes: Route[] = [];
|
85
|
+
private _wsRoutes: WSRoute[] = [];
|
57
86
|
private plugins: BXO[] = [];
|
58
87
|
private hooks: LifecycleHooks = {};
|
59
88
|
private server?: any;
|
@@ -61,51 +90,53 @@ export default class BXO {
|
|
61
90
|
private hotReloadEnabled: boolean = false;
|
62
91
|
private watchedFiles: Set<string> = new Set();
|
63
92
|
private watchedExclude: Set<string> = new Set();
|
93
|
+
private serverPort?: number;
|
94
|
+
private serverHostname?: string;
|
64
95
|
|
65
96
|
constructor() { }
|
66
97
|
|
67
98
|
// Lifecycle hook methods
|
68
|
-
onBeforeStart(handler: () => Promise<void> | void): this {
|
99
|
+
onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
|
69
100
|
this.hooks.onBeforeStart = handler;
|
70
101
|
return this;
|
71
102
|
}
|
72
103
|
|
73
|
-
onAfterStart(handler: () => Promise<void> | void): this {
|
104
|
+
onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
|
74
105
|
this.hooks.onAfterStart = handler;
|
75
106
|
return this;
|
76
107
|
}
|
77
108
|
|
78
|
-
onBeforeStop(handler: () => Promise<void> | void): this {
|
109
|
+
onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
|
79
110
|
this.hooks.onBeforeStop = handler;
|
80
111
|
return this;
|
81
112
|
}
|
82
113
|
|
83
|
-
onAfterStop(handler: () => Promise<void> | void): this {
|
114
|
+
onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
|
84
115
|
this.hooks.onAfterStop = handler;
|
85
116
|
return this;
|
86
117
|
}
|
87
118
|
|
88
|
-
onBeforeRestart(handler: () => Promise<void> | void): this {
|
119
|
+
onBeforeRestart(handler: (instance: BXO) => Promise<void> | void): this {
|
89
120
|
this.hooks.onBeforeRestart = handler;
|
90
121
|
return this;
|
91
122
|
}
|
92
123
|
|
93
|
-
onAfterRestart(handler: () => Promise<void> | void): this {
|
124
|
+
onAfterRestart(handler: (instance: BXO) => Promise<void> | void): this {
|
94
125
|
this.hooks.onAfterRestart = handler;
|
95
126
|
return this;
|
96
127
|
}
|
97
128
|
|
98
|
-
onRequest(handler: (ctx: Context) => Promise<void> | void): this {
|
129
|
+
onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
|
99
130
|
this.hooks.onRequest = handler;
|
100
131
|
return this;
|
101
132
|
}
|
102
133
|
|
103
|
-
onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
|
134
|
+
onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
|
104
135
|
this.hooks.onResponse = handler;
|
105
136
|
return this;
|
106
137
|
}
|
107
138
|
|
108
|
-
onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
|
139
|
+
onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
|
109
140
|
this.hooks.onError = handler;
|
110
141
|
return this;
|
111
142
|
}
|
@@ -131,7 +162,7 @@ export default class BXO {
|
|
131
162
|
handler: Handler<TConfig>,
|
132
163
|
config?: TConfig
|
133
164
|
): this {
|
134
|
-
this.
|
165
|
+
this._routes.push({ method: 'GET', path, handler, config });
|
135
166
|
return this;
|
136
167
|
}
|
137
168
|
|
@@ -149,7 +180,7 @@ export default class BXO {
|
|
149
180
|
handler: Handler<TConfig>,
|
150
181
|
config?: TConfig
|
151
182
|
): this {
|
152
|
-
this.
|
183
|
+
this._routes.push({ method: 'POST', path, handler, config });
|
153
184
|
return this;
|
154
185
|
}
|
155
186
|
|
@@ -167,7 +198,7 @@ export default class BXO {
|
|
167
198
|
handler: Handler<TConfig>,
|
168
199
|
config?: TConfig
|
169
200
|
): this {
|
170
|
-
this.
|
201
|
+
this._routes.push({ method: 'PUT', path, handler, config });
|
171
202
|
return this;
|
172
203
|
}
|
173
204
|
|
@@ -185,7 +216,7 @@ export default class BXO {
|
|
185
216
|
handler: Handler<TConfig>,
|
186
217
|
config?: TConfig
|
187
218
|
): this {
|
188
|
-
this.
|
219
|
+
this._routes.push({ method: 'DELETE', path, handler, config });
|
189
220
|
return this;
|
190
221
|
}
|
191
222
|
|
@@ -203,28 +234,118 @@ export default class BXO {
|
|
203
234
|
handler: Handler<TConfig>,
|
204
235
|
config?: TConfig
|
205
236
|
): this {
|
206
|
-
this.
|
237
|
+
this._routes.push({ method: 'PATCH', path, handler, config });
|
238
|
+
return this;
|
239
|
+
}
|
240
|
+
|
241
|
+
// WebSocket route handler
|
242
|
+
ws(path: string, handler: WebSocketHandler): this {
|
243
|
+
this._wsRoutes.push({ path, handler });
|
207
244
|
return this;
|
208
245
|
}
|
209
246
|
|
210
247
|
// Route matching utility
|
211
248
|
private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
|
212
|
-
for (const route of this.
|
249
|
+
for (const route of this._routes) {
|
213
250
|
if (route.method !== method) continue;
|
214
251
|
|
215
252
|
const routeSegments = route.path.split('/').filter(Boolean);
|
216
253
|
const pathSegments = pathname.split('/').filter(Boolean);
|
217
254
|
|
218
|
-
|
255
|
+
const params: Record<string, string> = {};
|
256
|
+
let isMatch = true;
|
257
|
+
|
258
|
+
// Handle wildcard at the end (catch-all)
|
259
|
+
const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
|
260
|
+
|
261
|
+
if (hasWildcardAtEnd) {
|
262
|
+
// For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
|
263
|
+
if (pathSegments.length < routeSegments.length - 1) continue;
|
264
|
+
} else {
|
265
|
+
// For exact matching (with possible single-segment wildcards), lengths must match
|
266
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
267
|
+
}
|
268
|
+
|
269
|
+
for (let i = 0; i < routeSegments.length; i++) {
|
270
|
+
const routeSegment = routeSegments[i];
|
271
|
+
const pathSegment = pathSegments[i];
|
272
|
+
|
273
|
+
if (!routeSegment) {
|
274
|
+
isMatch = false;
|
275
|
+
break;
|
276
|
+
}
|
277
|
+
|
278
|
+
// Handle catch-all wildcard at the end
|
279
|
+
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
280
|
+
// Wildcard at end matches remaining path segments
|
281
|
+
const remainingPath = pathSegments.slice(i).join('/');
|
282
|
+
params['*'] = remainingPath;
|
283
|
+
break;
|
284
|
+
}
|
285
|
+
|
286
|
+
if (!pathSegment) {
|
287
|
+
isMatch = false;
|
288
|
+
break;
|
289
|
+
}
|
290
|
+
|
291
|
+
if (routeSegment.startsWith(':')) {
|
292
|
+
const paramName = routeSegment.slice(1);
|
293
|
+
params[paramName] = decodeURIComponent(pathSegment);
|
294
|
+
} else if (routeSegment === '*') {
|
295
|
+
// Single segment wildcard
|
296
|
+
params['*'] = decodeURIComponent(pathSegment);
|
297
|
+
} else if (routeSegment !== pathSegment) {
|
298
|
+
isMatch = false;
|
299
|
+
break;
|
300
|
+
}
|
301
|
+
}
|
302
|
+
|
303
|
+
if (isMatch) {
|
304
|
+
return { route, params };
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
308
|
+
return null;
|
309
|
+
}
|
310
|
+
|
311
|
+
// WebSocket route matching utility
|
312
|
+
private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
|
313
|
+
for (const route of this._wsRoutes) {
|
314
|
+
const routeSegments = route.path.split('/').filter(Boolean);
|
315
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
219
316
|
|
220
317
|
const params: Record<string, string> = {};
|
221
318
|
let isMatch = true;
|
222
319
|
|
320
|
+
// Handle wildcard at the end (catch-all)
|
321
|
+
const hasWildcardAtEnd = routeSegments.length > 0 && routeSegments[routeSegments.length - 1] === '*';
|
322
|
+
|
323
|
+
if (hasWildcardAtEnd) {
|
324
|
+
// For catch-all wildcard, path must have at least as many segments as route (minus the wildcard)
|
325
|
+
if (pathSegments.length < routeSegments.length - 1) continue;
|
326
|
+
} else {
|
327
|
+
// For exact matching (with possible single-segment wildcards), lengths must match
|
328
|
+
if (routeSegments.length !== pathSegments.length) continue;
|
329
|
+
}
|
330
|
+
|
223
331
|
for (let i = 0; i < routeSegments.length; i++) {
|
224
332
|
const routeSegment = routeSegments[i];
|
225
333
|
const pathSegment = pathSegments[i];
|
226
334
|
|
227
|
-
if (!routeSegment
|
335
|
+
if (!routeSegment) {
|
336
|
+
isMatch = false;
|
337
|
+
break;
|
338
|
+
}
|
339
|
+
|
340
|
+
// Handle catch-all wildcard at the end
|
341
|
+
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
342
|
+
// Wildcard at end matches remaining path segments
|
343
|
+
const remainingPath = pathSegments.slice(i).join('/');
|
344
|
+
params['*'] = remainingPath;
|
345
|
+
break;
|
346
|
+
}
|
347
|
+
|
348
|
+
if (!pathSegment) {
|
228
349
|
isMatch = false;
|
229
350
|
break;
|
230
351
|
}
|
@@ -232,6 +353,9 @@ export default class BXO {
|
|
232
353
|
if (routeSegment.startsWith(':')) {
|
233
354
|
const paramName = routeSegment.slice(1);
|
234
355
|
params[paramName] = decodeURIComponent(pathSegment);
|
356
|
+
} else if (routeSegment === '*') {
|
357
|
+
// Single segment wildcard
|
358
|
+
params['*'] = decodeURIComponent(pathSegment);
|
235
359
|
} else if (routeSegment !== pathSegment) {
|
236
360
|
isMatch = false;
|
237
361
|
break;
|
@@ -271,11 +395,30 @@ export default class BXO {
|
|
271
395
|
}
|
272
396
|
|
273
397
|
// Main request handler
|
274
|
-
private async handleRequest(request: Request): Promise<Response> {
|
398
|
+
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
275
399
|
const url = new URL(request.url);
|
276
400
|
const method = request.method;
|
277
401
|
const pathname = url.pathname;
|
278
402
|
|
403
|
+
// Check for WebSocket upgrade
|
404
|
+
if (request.headers.get('upgrade') === 'websocket') {
|
405
|
+
const wsMatchResult = this.matchWSRoute(pathname);
|
406
|
+
if (wsMatchResult && server) {
|
407
|
+
const success = server.upgrade(request, {
|
408
|
+
data: {
|
409
|
+
handler: wsMatchResult.route.handler,
|
410
|
+
params: wsMatchResult.params,
|
411
|
+
pathname
|
412
|
+
}
|
413
|
+
});
|
414
|
+
|
415
|
+
if (success) {
|
416
|
+
return; // undefined response means upgrade was successful
|
417
|
+
}
|
418
|
+
}
|
419
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
420
|
+
}
|
421
|
+
|
279
422
|
const matchResult = this.matchRoute(method, pathname);
|
280
423
|
if (!matchResult) {
|
281
424
|
return new Response('Not Found', { status: 404 });
|
@@ -308,6 +451,7 @@ export default class BXO {
|
|
308
451
|
query: route.config?.query ? this.validateData(route.config.query, query) : query,
|
309
452
|
body: route.config?.body ? this.validateData(route.config.body, body) : body,
|
310
453
|
headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
|
454
|
+
path: pathname,
|
311
455
|
request,
|
312
456
|
set: {}
|
313
457
|
};
|
@@ -315,13 +459,13 @@ export default class BXO {
|
|
315
459
|
try {
|
316
460
|
// Run global onRequest hook
|
317
461
|
if (this.hooks.onRequest) {
|
318
|
-
await this.hooks.onRequest(ctx);
|
462
|
+
await this.hooks.onRequest(ctx, this);
|
319
463
|
}
|
320
464
|
|
321
465
|
// Run BXO instance onRequest hooks
|
322
466
|
for (const bxoInstance of this.plugins) {
|
323
467
|
if (bxoInstance.hooks.onRequest) {
|
324
|
-
await bxoInstance.hooks.onRequest(ctx);
|
468
|
+
await bxoInstance.hooks.onRequest(ctx, this);
|
325
469
|
}
|
326
470
|
}
|
327
471
|
|
@@ -330,13 +474,13 @@ export default class BXO {
|
|
330
474
|
|
331
475
|
// Run global onResponse hook
|
332
476
|
if (this.hooks.onResponse) {
|
333
|
-
response = await this.hooks.onResponse(ctx, response) || response;
|
477
|
+
response = await this.hooks.onResponse(ctx, response, this) || response;
|
334
478
|
}
|
335
479
|
|
336
480
|
// Run BXO instance onResponse hooks
|
337
481
|
for (const bxoInstance of this.plugins) {
|
338
482
|
if (bxoInstance.hooks.onResponse) {
|
339
|
-
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
483
|
+
response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
|
340
484
|
}
|
341
485
|
}
|
342
486
|
|
@@ -359,6 +503,35 @@ export default class BXO {
|
|
359
503
|
return response;
|
360
504
|
}
|
361
505
|
|
506
|
+
// Handle File response (like Elysia)
|
507
|
+
if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
|
508
|
+
const file = response as File;
|
509
|
+
const responseInit: ResponseInit = {
|
510
|
+
status: ctx.set.status || 200,
|
511
|
+
headers: {
|
512
|
+
'Content-Type': file.type || 'application/octet-stream',
|
513
|
+
'Content-Length': file.size.toString(),
|
514
|
+
...ctx.set.headers
|
515
|
+
}
|
516
|
+
};
|
517
|
+
return new Response(file, responseInit);
|
518
|
+
}
|
519
|
+
|
520
|
+
// Handle Bun.file() response
|
521
|
+
if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
|
522
|
+
const bunFile = response as any;
|
523
|
+
const responseInit: ResponseInit = {
|
524
|
+
status: ctx.set.status || 200,
|
525
|
+
headers: {
|
526
|
+
'Content-Type': bunFile.type || 'application/octet-stream',
|
527
|
+
'Content-Length': bunFile.size?.toString() || '',
|
528
|
+
...ctx.set.headers,
|
529
|
+
...(bunFile.headers || {}) // Support custom headers from file helper
|
530
|
+
}
|
531
|
+
};
|
532
|
+
return new Response(bunFile, responseInit);
|
533
|
+
}
|
534
|
+
|
362
535
|
const responseInit: ResponseInit = {
|
363
536
|
status: ctx.set.status || 200,
|
364
537
|
headers: ctx.set.headers || {}
|
@@ -381,12 +554,12 @@ export default class BXO {
|
|
381
554
|
let errorResponse: any;
|
382
555
|
|
383
556
|
if (this.hooks.onError) {
|
384
|
-
errorResponse = await this.hooks.onError(ctx, error as Error);
|
557
|
+
errorResponse = await this.hooks.onError(ctx, error as Error, this);
|
385
558
|
}
|
386
559
|
|
387
560
|
for (const bxoInstance of this.plugins) {
|
388
561
|
if (bxoInstance.hooks.onError) {
|
389
|
-
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
562
|
+
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
|
390
563
|
}
|
391
564
|
}
|
392
565
|
|
@@ -423,14 +596,14 @@ export default class BXO {
|
|
423
596
|
if (pattern === filename) {
|
424
597
|
return true;
|
425
598
|
}
|
426
|
-
|
599
|
+
|
427
600
|
// Handle directory patterns (e.g., "node_modules/", "dist/")
|
428
601
|
if (pattern.endsWith('/')) {
|
429
602
|
if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
|
430
603
|
return true;
|
431
604
|
}
|
432
605
|
}
|
433
|
-
|
606
|
+
|
434
607
|
// Handle wildcard patterns (e.g., "*.log", "temp*")
|
435
608
|
if (pattern.includes('*')) {
|
436
609
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
@@ -438,18 +611,18 @@ export default class BXO {
|
|
438
611
|
return true;
|
439
612
|
}
|
440
613
|
}
|
441
|
-
|
614
|
+
|
442
615
|
// Handle file extension patterns (e.g., ".log", ".tmp")
|
443
616
|
if (pattern.startsWith('.') && filename.endsWith(pattern)) {
|
444
617
|
return true;
|
445
618
|
}
|
446
|
-
|
619
|
+
|
447
620
|
// Handle substring matches for directories
|
448
621
|
if (filename.includes(pattern)) {
|
449
622
|
return true;
|
450
623
|
}
|
451
624
|
}
|
452
|
-
|
625
|
+
|
453
626
|
return false;
|
454
627
|
}
|
455
628
|
|
@@ -466,7 +639,7 @@ export default class BXO {
|
|
466
639
|
if (this.shouldExcludeFile(filename)) {
|
467
640
|
return;
|
468
641
|
}
|
469
|
-
|
642
|
+
|
470
643
|
console.log(`🔄 File changed: ${filename}, restarting server...`);
|
471
644
|
await this.restart(port, hostname);
|
472
645
|
}
|
@@ -491,22 +664,44 @@ 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
696
|
this.isRunning = true;
|
697
|
+
this.serverPort = port;
|
698
|
+
this.serverHostname = hostname;
|
504
699
|
|
505
700
|
console.log(`🦊 BXO server running at http://${hostname}:${port}`);
|
506
701
|
|
507
702
|
// After start hook
|
508
703
|
if (this.hooks.onAfterStart) {
|
509
|
-
await this.hooks.onAfterStart();
|
704
|
+
await this.hooks.onAfterStart(this);
|
510
705
|
}
|
511
706
|
|
512
707
|
// Setup hot reload
|
@@ -536,7 +731,7 @@ 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) {
|
@@ -545,12 +740,14 @@ export default class BXO {
|
|
545
740
|
}
|
546
741
|
|
547
742
|
this.isRunning = false;
|
743
|
+
this.serverPort = undefined;
|
744
|
+
this.serverHostname = undefined;
|
548
745
|
|
549
746
|
console.log('🛑 BXO server stopped');
|
550
747
|
|
551
748
|
// After stop hook
|
552
749
|
if (this.hooks.onAfterStop) {
|
553
|
-
await this.hooks.onAfterStop();
|
750
|
+
await this.hooks.onAfterStop(this);
|
554
751
|
}
|
555
752
|
|
556
753
|
} catch (error) {
|
@@ -563,7 +760,7 @@ export default class BXO {
|
|
563
760
|
try {
|
564
761
|
// Before restart hook
|
565
762
|
if (this.hooks.onBeforeRestart) {
|
566
|
-
await this.hooks.onBeforeRestart();
|
763
|
+
await this.hooks.onBeforeRestart(this);
|
567
764
|
}
|
568
765
|
|
569
766
|
console.log('🔄 Restarting BXO server...');
|
@@ -577,7 +774,7 @@ export default class BXO {
|
|
577
774
|
|
578
775
|
// After restart hook
|
579
776
|
if (this.hooks.onAfterRestart) {
|
580
|
-
await this.hooks.onAfterRestart();
|
777
|
+
await this.hooks.onAfterRestart(this);
|
581
778
|
}
|
582
779
|
|
583
780
|
} catch (error) {
|
@@ -604,14 +801,85 @@ export default class BXO {
|
|
604
801
|
excludePatterns: Array.from(this.watchedExclude)
|
605
802
|
};
|
606
803
|
}
|
804
|
+
|
805
|
+
// Get server information (alias for getServerInfo)
|
806
|
+
get info() {
|
807
|
+
return {
|
808
|
+
// Server status
|
809
|
+
running: this.isRunning,
|
810
|
+
server: this.server ? 'Bun' : null,
|
811
|
+
|
812
|
+
// Connection details
|
813
|
+
hostname: this.serverHostname,
|
814
|
+
port: this.serverPort,
|
815
|
+
url: this.isRunning && this.serverHostname && this.serverPort
|
816
|
+
? `http://${this.serverHostname}:${this.serverPort}`
|
817
|
+
: null,
|
818
|
+
|
819
|
+
// Application statistics
|
820
|
+
totalRoutes: this._routes.length,
|
821
|
+
totalWsRoutes: this._wsRoutes.length,
|
822
|
+
totalPlugins: this.plugins.length,
|
823
|
+
|
824
|
+
// Hot reload configuration
|
825
|
+
hotReload: this.hotReloadEnabled,
|
826
|
+
watchedFiles: Array.from(this.watchedFiles),
|
827
|
+
excludePatterns: Array.from(this.watchedExclude),
|
828
|
+
|
829
|
+
// System information
|
830
|
+
runtime: 'Bun',
|
831
|
+
version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
|
832
|
+
pid: process.pid,
|
833
|
+
uptime: this.isRunning ? process.uptime() : 0
|
834
|
+
};
|
835
|
+
}
|
836
|
+
|
837
|
+
// Get all routes information
|
838
|
+
get routes() {
|
839
|
+
return this._routes.map((route: Route) => ({
|
840
|
+
method: route.method,
|
841
|
+
path: route.path,
|
842
|
+
hasConfig: !!route.config,
|
843
|
+
config: route.config || null
|
844
|
+
}));
|
845
|
+
}
|
846
|
+
|
847
|
+
// Get all WebSocket routes information
|
848
|
+
get wsRoutes() {
|
849
|
+
return this._wsRoutes.map((route: WSRoute) => ({
|
850
|
+
path: route.path,
|
851
|
+
hasHandlers: {
|
852
|
+
onOpen: !!route.handler.onOpen,
|
853
|
+
onMessage: !!route.handler.onMessage,
|
854
|
+
onClose: !!route.handler.onClose,
|
855
|
+
onError: !!route.handler.onError
|
856
|
+
}
|
857
|
+
}));
|
858
|
+
}
|
859
|
+
}
|
860
|
+
|
861
|
+
const error = (error: Error | string, status: number = 500) => {
|
862
|
+
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
|
607
863
|
}
|
608
864
|
|
609
|
-
|
610
|
-
|
865
|
+
// File helper function (like Elysia)
|
866
|
+
const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
|
867
|
+
const bunFile = Bun.file(path);
|
868
|
+
|
869
|
+
if (options?.type) {
|
870
|
+
// Create a wrapper to override the MIME type
|
871
|
+
return {
|
872
|
+
...bunFile,
|
873
|
+
type: options.type,
|
874
|
+
headers: options.headers
|
875
|
+
};
|
876
|
+
}
|
877
|
+
|
878
|
+
return bunFile;
|
611
879
|
}
|
612
880
|
|
613
881
|
// Export Zod for convenience
|
614
|
-
export { z, error };
|
882
|
+
export { z, error, file };
|
615
883
|
|
616
884
|
// Export types for external use
|
617
|
-
export type { RouteConfig, Handler };
|
885
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
|