bxo 0.0.4 → 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 +378 -48
- package/package.json +1 -1
package/index.ts
CHANGED
@@ -3,12 +3,26 @@ 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>;
|
9
21
|
query?: z.ZodSchema<any>;
|
10
22
|
body?: z.ZodSchema<any>;
|
11
23
|
headers?: z.ZodSchema<any>;
|
24
|
+
response?: z.ZodSchema<any>;
|
25
|
+
detail?: RouteDetail;
|
12
26
|
}
|
13
27
|
|
14
28
|
// Context type that's fully typed based on the route configuration
|
@@ -17,6 +31,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
17
31
|
query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
|
18
32
|
body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
|
19
33
|
headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
|
34
|
+
path: string;
|
20
35
|
request: Request;
|
21
36
|
set: {
|
22
37
|
status?: number;
|
@@ -38,72 +53,90 @@ interface Route {
|
|
38
53
|
config?: RouteConfig;
|
39
54
|
}
|
40
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
|
+
|
41
70
|
// Lifecycle hooks
|
42
71
|
interface LifecycleHooks {
|
43
|
-
onBeforeStart?: () => Promise<void> | void;
|
44
|
-
onAfterStart?: () => Promise<void> | void;
|
45
|
-
onBeforeStop?: () => Promise<void> | void;
|
46
|
-
onAfterStop?: () => Promise<void> | void;
|
47
|
-
onBeforeRestart?: () => Promise<void> | void;
|
48
|
-
onAfterRestart?: () => Promise<void> | void;
|
49
|
-
onRequest?: (ctx: Context) => Promise<void> | void;
|
50
|
-
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
51
|
-
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;
|
52
81
|
}
|
53
82
|
|
54
83
|
export default class BXO {
|
55
|
-
private
|
84
|
+
private _routes: Route[] = [];
|
85
|
+
private _wsRoutes: WSRoute[] = [];
|
56
86
|
private plugins: BXO[] = [];
|
57
87
|
private hooks: LifecycleHooks = {};
|
58
88
|
private server?: any;
|
59
89
|
private isRunning: boolean = false;
|
60
90
|
private hotReloadEnabled: boolean = false;
|
61
91
|
private watchedFiles: Set<string> = new Set();
|
92
|
+
private watchedExclude: Set<string> = new Set();
|
93
|
+
private serverPort?: number;
|
94
|
+
private serverHostname?: string;
|
62
95
|
|
63
96
|
constructor() { }
|
64
97
|
|
65
98
|
// Lifecycle hook methods
|
66
|
-
onBeforeStart(handler: () => Promise<void> | void): this {
|
99
|
+
onBeforeStart(handler: (instance: BXO) => Promise<void> | void): this {
|
67
100
|
this.hooks.onBeforeStart = handler;
|
68
101
|
return this;
|
69
102
|
}
|
70
103
|
|
71
|
-
onAfterStart(handler: () => Promise<void> | void): this {
|
104
|
+
onAfterStart(handler: (instance: BXO) => Promise<void> | void): this {
|
72
105
|
this.hooks.onAfterStart = handler;
|
73
106
|
return this;
|
74
107
|
}
|
75
108
|
|
76
|
-
onBeforeStop(handler: () => Promise<void> | void): this {
|
109
|
+
onBeforeStop(handler: (instance: BXO) => Promise<void> | void): this {
|
77
110
|
this.hooks.onBeforeStop = handler;
|
78
111
|
return this;
|
79
112
|
}
|
80
113
|
|
81
|
-
onAfterStop(handler: () => Promise<void> | void): this {
|
114
|
+
onAfterStop(handler: (instance: BXO) => Promise<void> | void): this {
|
82
115
|
this.hooks.onAfterStop = handler;
|
83
116
|
return this;
|
84
117
|
}
|
85
118
|
|
86
|
-
onBeforeRestart(handler: () => Promise<void> | void): this {
|
119
|
+
onBeforeRestart(handler: (instance: BXO) => Promise<void> | void): this {
|
87
120
|
this.hooks.onBeforeRestart = handler;
|
88
121
|
return this;
|
89
122
|
}
|
90
123
|
|
91
|
-
onAfterRestart(handler: () => Promise<void> | void): this {
|
124
|
+
onAfterRestart(handler: (instance: BXO) => Promise<void> | void): this {
|
92
125
|
this.hooks.onAfterRestart = handler;
|
93
126
|
return this;
|
94
127
|
}
|
95
128
|
|
96
|
-
onRequest(handler: (ctx: Context) => Promise<void> | void): this {
|
129
|
+
onRequest(handler: (ctx: Context, instance: BXO) => Promise<void> | void): this {
|
97
130
|
this.hooks.onRequest = handler;
|
98
131
|
return this;
|
99
132
|
}
|
100
133
|
|
101
|
-
onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
|
134
|
+
onResponse(handler: (ctx: Context, response: any, instance: BXO) => Promise<any> | any): this {
|
102
135
|
this.hooks.onResponse = handler;
|
103
136
|
return this;
|
104
137
|
}
|
105
138
|
|
106
|
-
onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
|
139
|
+
onError(handler: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any): this {
|
107
140
|
this.hooks.onError = handler;
|
108
141
|
return this;
|
109
142
|
}
|
@@ -129,7 +162,7 @@ export default class BXO {
|
|
129
162
|
handler: Handler<TConfig>,
|
130
163
|
config?: TConfig
|
131
164
|
): this {
|
132
|
-
this.
|
165
|
+
this._routes.push({ method: 'GET', path, handler, config });
|
133
166
|
return this;
|
134
167
|
}
|
135
168
|
|
@@ -147,7 +180,7 @@ export default class BXO {
|
|
147
180
|
handler: Handler<TConfig>,
|
148
181
|
config?: TConfig
|
149
182
|
): this {
|
150
|
-
this.
|
183
|
+
this._routes.push({ method: 'POST', path, handler, config });
|
151
184
|
return this;
|
152
185
|
}
|
153
186
|
|
@@ -165,7 +198,7 @@ export default class BXO {
|
|
165
198
|
handler: Handler<TConfig>,
|
166
199
|
config?: TConfig
|
167
200
|
): this {
|
168
|
-
this.
|
201
|
+
this._routes.push({ method: 'PUT', path, handler, config });
|
169
202
|
return this;
|
170
203
|
}
|
171
204
|
|
@@ -183,7 +216,7 @@ export default class BXO {
|
|
183
216
|
handler: Handler<TConfig>,
|
184
217
|
config?: TConfig
|
185
218
|
): this {
|
186
|
-
this.
|
219
|
+
this._routes.push({ method: 'DELETE', path, handler, config });
|
187
220
|
return this;
|
188
221
|
}
|
189
222
|
|
@@ -201,28 +234,118 @@ export default class BXO {
|
|
201
234
|
handler: Handler<TConfig>,
|
202
235
|
config?: TConfig
|
203
236
|
): this {
|
204
|
-
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 });
|
205
244
|
return this;
|
206
245
|
}
|
207
246
|
|
208
247
|
// Route matching utility
|
209
248
|
private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
|
210
|
-
for (const route of this.
|
249
|
+
for (const route of this._routes) {
|
211
250
|
if (route.method !== method) continue;
|
212
251
|
|
213
252
|
const routeSegments = route.path.split('/').filter(Boolean);
|
214
253
|
const pathSegments = pathname.split('/').filter(Boolean);
|
215
254
|
|
216
|
-
|
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);
|
217
316
|
|
218
317
|
const params: Record<string, string> = {};
|
219
318
|
let isMatch = true;
|
220
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
|
+
|
221
331
|
for (let i = 0; i < routeSegments.length; i++) {
|
222
332
|
const routeSegment = routeSegments[i];
|
223
333
|
const pathSegment = pathSegments[i];
|
224
334
|
|
225
|
-
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) {
|
226
349
|
isMatch = false;
|
227
350
|
break;
|
228
351
|
}
|
@@ -230,6 +353,9 @@ export default class BXO {
|
|
230
353
|
if (routeSegment.startsWith(':')) {
|
231
354
|
const paramName = routeSegment.slice(1);
|
232
355
|
params[paramName] = decodeURIComponent(pathSegment);
|
356
|
+
} else if (routeSegment === '*') {
|
357
|
+
// Single segment wildcard
|
358
|
+
params['*'] = decodeURIComponent(pathSegment);
|
233
359
|
} else if (routeSegment !== pathSegment) {
|
234
360
|
isMatch = false;
|
235
361
|
break;
|
@@ -269,11 +395,30 @@ export default class BXO {
|
|
269
395
|
}
|
270
396
|
|
271
397
|
// Main request handler
|
272
|
-
private async handleRequest(request: Request): Promise<Response> {
|
398
|
+
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
273
399
|
const url = new URL(request.url);
|
274
400
|
const method = request.method;
|
275
401
|
const pathname = url.pathname;
|
276
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
|
+
|
277
422
|
const matchResult = this.matchRoute(method, pathname);
|
278
423
|
if (!matchResult) {
|
279
424
|
return new Response('Not Found', { status: 404 });
|
@@ -306,6 +451,7 @@ export default class BXO {
|
|
306
451
|
query: route.config?.query ? this.validateData(route.config.query, query) : query,
|
307
452
|
body: route.config?.body ? this.validateData(route.config.body, body) : body,
|
308
453
|
headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
|
454
|
+
path: pathname,
|
309
455
|
request,
|
310
456
|
set: {}
|
311
457
|
};
|
@@ -313,13 +459,13 @@ export default class BXO {
|
|
313
459
|
try {
|
314
460
|
// Run global onRequest hook
|
315
461
|
if (this.hooks.onRequest) {
|
316
|
-
await this.hooks.onRequest(ctx);
|
462
|
+
await this.hooks.onRequest(ctx, this);
|
317
463
|
}
|
318
464
|
|
319
465
|
// Run BXO instance onRequest hooks
|
320
466
|
for (const bxoInstance of this.plugins) {
|
321
467
|
if (bxoInstance.hooks.onRequest) {
|
322
|
-
await bxoInstance.hooks.onRequest(ctx);
|
468
|
+
await bxoInstance.hooks.onRequest(ctx, this);
|
323
469
|
}
|
324
470
|
}
|
325
471
|
|
@@ -328,13 +474,27 @@ export default class BXO {
|
|
328
474
|
|
329
475
|
// Run global onResponse hook
|
330
476
|
if (this.hooks.onResponse) {
|
331
|
-
response = await this.hooks.onResponse(ctx, response) || response;
|
477
|
+
response = await this.hooks.onResponse(ctx, response, this) || response;
|
332
478
|
}
|
333
479
|
|
334
480
|
// Run BXO instance onResponse hooks
|
335
481
|
for (const bxoInstance of this.plugins) {
|
336
482
|
if (bxoInstance.hooks.onResponse) {
|
337
|
-
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
483
|
+
response = await bxoInstance.hooks.onResponse(ctx, response, this) || response;
|
484
|
+
}
|
485
|
+
}
|
486
|
+
|
487
|
+
// Validate response against schema if provided
|
488
|
+
if (route.config?.response && !(response instanceof Response)) {
|
489
|
+
try {
|
490
|
+
response = this.validateData(route.config.response, response);
|
491
|
+
} catch (validationError) {
|
492
|
+
// Response validation failed
|
493
|
+
const errorMessage = validationError instanceof Error ? validationError.message : 'Response validation failed';
|
494
|
+
return new Response(JSON.stringify({ error: `Response validation error: ${errorMessage}` }), {
|
495
|
+
status: 500,
|
496
|
+
headers: { 'Content-Type': 'application/json' }
|
497
|
+
});
|
338
498
|
}
|
339
499
|
}
|
340
500
|
|
@@ -343,6 +503,35 @@ export default class BXO {
|
|
343
503
|
return response;
|
344
504
|
}
|
345
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
|
+
|
346
535
|
const responseInit: ResponseInit = {
|
347
536
|
status: ctx.set.status || 200,
|
348
537
|
headers: ctx.set.headers || {}
|
@@ -365,12 +554,12 @@ export default class BXO {
|
|
365
554
|
let errorResponse: any;
|
366
555
|
|
367
556
|
if (this.hooks.onError) {
|
368
|
-
errorResponse = await this.hooks.onError(ctx, error as Error);
|
557
|
+
errorResponse = await this.hooks.onError(ctx, error as Error, this);
|
369
558
|
}
|
370
559
|
|
371
560
|
for (const bxoInstance of this.plugins) {
|
372
561
|
if (bxoInstance.hooks.onError) {
|
373
|
-
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
562
|
+
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error, this) || errorResponse;
|
374
563
|
}
|
375
564
|
}
|
376
565
|
|
@@ -394,12 +583,49 @@ export default class BXO {
|
|
394
583
|
}
|
395
584
|
|
396
585
|
// Hot reload functionality
|
397
|
-
enableHotReload(watchPaths: string[] = ['./']): this {
|
586
|
+
enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
|
398
587
|
this.hotReloadEnabled = true;
|
399
588
|
watchPaths.forEach(path => this.watchedFiles.add(path));
|
589
|
+
excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
|
400
590
|
return this;
|
401
591
|
}
|
402
592
|
|
593
|
+
private shouldExcludeFile(filename: string): boolean {
|
594
|
+
for (const pattern of this.watchedExclude) {
|
595
|
+
// Handle exact match
|
596
|
+
if (pattern === filename) {
|
597
|
+
return true;
|
598
|
+
}
|
599
|
+
|
600
|
+
// Handle directory patterns (e.g., "node_modules/", "dist/")
|
601
|
+
if (pattern.endsWith('/')) {
|
602
|
+
if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
|
603
|
+
return true;
|
604
|
+
}
|
605
|
+
}
|
606
|
+
|
607
|
+
// Handle wildcard patterns (e.g., "*.log", "temp*")
|
608
|
+
if (pattern.includes('*')) {
|
609
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
610
|
+
if (regex.test(filename)) {
|
611
|
+
return true;
|
612
|
+
}
|
613
|
+
}
|
614
|
+
|
615
|
+
// Handle file extension patterns (e.g., ".log", ".tmp")
|
616
|
+
if (pattern.startsWith('.') && filename.endsWith(pattern)) {
|
617
|
+
return true;
|
618
|
+
}
|
619
|
+
|
620
|
+
// Handle substring matches for directories
|
621
|
+
if (filename.includes(pattern)) {
|
622
|
+
return true;
|
623
|
+
}
|
624
|
+
}
|
625
|
+
|
626
|
+
return false;
|
627
|
+
}
|
628
|
+
|
403
629
|
private async setupFileWatcher(port: number, hostname: string): Promise<void> {
|
404
630
|
if (!this.hotReloadEnabled) return;
|
405
631
|
|
@@ -409,11 +635,19 @@ export default class BXO {
|
|
409
635
|
try {
|
410
636
|
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
411
637
|
if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
|
638
|
+
// Check if file should be excluded
|
639
|
+
if (this.shouldExcludeFile(filename)) {
|
640
|
+
return;
|
641
|
+
}
|
642
|
+
|
412
643
|
console.log(`🔄 File changed: ${filename}, restarting server...`);
|
413
644
|
await this.restart(port, hostname);
|
414
645
|
}
|
415
646
|
});
|
416
647
|
console.log(`👀 Watching ${watchPath} for changes...`);
|
648
|
+
if (this.watchedExclude.size > 0) {
|
649
|
+
console.log(`🚫 Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
|
650
|
+
}
|
417
651
|
} catch (error) {
|
418
652
|
console.warn(`⚠️ Could not watch ${watchPath}:`, error);
|
419
653
|
}
|
@@ -430,22 +664,44 @@ export default class BXO {
|
|
430
664
|
try {
|
431
665
|
// Before start hook
|
432
666
|
if (this.hooks.onBeforeStart) {
|
433
|
-
await this.hooks.onBeforeStart();
|
667
|
+
await this.hooks.onBeforeStart(this);
|
434
668
|
}
|
435
669
|
|
436
670
|
this.server = Bun.serve({
|
437
671
|
port,
|
438
672
|
hostname,
|
439
|
-
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
|
+
}
|
440
694
|
});
|
441
695
|
|
442
696
|
this.isRunning = true;
|
697
|
+
this.serverPort = port;
|
698
|
+
this.serverHostname = hostname;
|
443
699
|
|
444
700
|
console.log(`🦊 BXO server running at http://${hostname}:${port}`);
|
445
701
|
|
446
702
|
// After start hook
|
447
703
|
if (this.hooks.onAfterStart) {
|
448
|
-
await this.hooks.onAfterStart();
|
704
|
+
await this.hooks.onAfterStart(this);
|
449
705
|
}
|
450
706
|
|
451
707
|
// Setup hot reload
|
@@ -475,7 +731,7 @@ export default class BXO {
|
|
475
731
|
try {
|
476
732
|
// Before stop hook
|
477
733
|
if (this.hooks.onBeforeStop) {
|
478
|
-
await this.hooks.onBeforeStop();
|
734
|
+
await this.hooks.onBeforeStop(this);
|
479
735
|
}
|
480
736
|
|
481
737
|
if (this.server) {
|
@@ -484,12 +740,14 @@ export default class BXO {
|
|
484
740
|
}
|
485
741
|
|
486
742
|
this.isRunning = false;
|
743
|
+
this.serverPort = undefined;
|
744
|
+
this.serverHostname = undefined;
|
487
745
|
|
488
746
|
console.log('🛑 BXO server stopped');
|
489
747
|
|
490
748
|
// After stop hook
|
491
749
|
if (this.hooks.onAfterStop) {
|
492
|
-
await this.hooks.onAfterStop();
|
750
|
+
await this.hooks.onAfterStop(this);
|
493
751
|
}
|
494
752
|
|
495
753
|
} catch (error) {
|
@@ -502,7 +760,7 @@ export default class BXO {
|
|
502
760
|
try {
|
503
761
|
// Before restart hook
|
504
762
|
if (this.hooks.onBeforeRestart) {
|
505
|
-
await this.hooks.onBeforeRestart();
|
763
|
+
await this.hooks.onBeforeRestart(this);
|
506
764
|
}
|
507
765
|
|
508
766
|
console.log('🔄 Restarting BXO server...');
|
@@ -516,7 +774,7 @@ export default class BXO {
|
|
516
774
|
|
517
775
|
// After restart hook
|
518
776
|
if (this.hooks.onAfterRestart) {
|
519
|
-
await this.hooks.onAfterRestart();
|
777
|
+
await this.hooks.onAfterRestart(this);
|
520
778
|
}
|
521
779
|
|
522
780
|
} catch (error) {
|
@@ -535,21 +793,93 @@ export default class BXO {
|
|
535
793
|
return this.isRunning;
|
536
794
|
}
|
537
795
|
|
538
|
-
getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[] } {
|
796
|
+
getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
|
539
797
|
return {
|
540
798
|
running: this.isRunning,
|
541
799
|
hotReload: this.hotReloadEnabled,
|
542
|
-
watchedFiles: Array.from(this.watchedFiles)
|
800
|
+
watchedFiles: Array.from(this.watchedFiles),
|
801
|
+
excludePatterns: Array.from(this.watchedExclude)
|
543
802
|
};
|
544
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 });
|
545
863
|
}
|
546
864
|
|
547
|
-
|
548
|
-
|
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;
|
549
879
|
}
|
550
880
|
|
551
881
|
// Export Zod for convenience
|
552
|
-
export { z, error };
|
882
|
+
export { z, error, file };
|
553
883
|
|
554
884
|
// Export types for external use
|
555
|
-
export type { RouteConfig };
|
885
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
|