bxo 0.0.5-dev.9 → 0.0.6

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 DELETED
@@ -1,885 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- // Type utilities for extracting types from Zod schemas
4
- type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
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
-
18
- // Configuration interface for route handlers
19
- interface RouteConfig {
20
- params?: z.ZodSchema<any>;
21
- query?: z.ZodSchema<any>;
22
- body?: z.ZodSchema<any>;
23
- headers?: z.ZodSchema<any>;
24
- response?: z.ZodSchema<any>;
25
- detail?: RouteDetail;
26
- }
27
-
28
- // Context type that's fully typed based on the route configuration
29
- export type Context<TConfig extends RouteConfig = {}> = {
30
- params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
31
- query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
32
- body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
33
- headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
34
- path: string;
35
- request: Request;
36
- set: {
37
- status?: number;
38
- headers?: Record<string, string>;
39
- };
40
- // Extended properties that can be added by plugins
41
- user?: any;
42
- [key: string]: any;
43
- };
44
-
45
- // Handler function type
46
- type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
47
-
48
- // Route definition
49
- interface Route {
50
- method: string;
51
- path: string;
52
- handler: Handler<any>;
53
- config?: RouteConfig;
54
- }
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
-
70
- // Lifecycle hooks
71
- interface LifecycleHooks {
72
- onBeforeStart?: () => Promise<void> | void;
73
- onAfterStart?: () => Promise<void> | void;
74
- onBeforeStop?: () => Promise<void> | void;
75
- onAfterStop?: () => Promise<void> | void;
76
- onBeforeRestart?: () => Promise<void> | void;
77
- onAfterRestart?: () => Promise<void> | void;
78
- onRequest?: (ctx: Context) => Promise<void> | void;
79
- onResponse?: (ctx: Context, response: any) => Promise<any> | any;
80
- onError?: (ctx: Context, error: Error) => Promise<any> | any;
81
- }
82
-
83
- export default class BXO {
84
- private _routes: Route[] = [];
85
- private _wsRoutes: WSRoute[] = [];
86
- private plugins: BXO[] = [];
87
- private hooks: LifecycleHooks = {};
88
- private server?: any;
89
- private isRunning: boolean = false;
90
- private hotReloadEnabled: boolean = false;
91
- private watchedFiles: Set<string> = new Set();
92
- private watchedExclude: Set<string> = new Set();
93
- private serverPort?: number;
94
- private serverHostname?: string;
95
-
96
- constructor() { }
97
-
98
- // Lifecycle hook methods
99
- onBeforeStart(handler: () => Promise<void> | void): this {
100
- this.hooks.onBeforeStart = handler;
101
- return this;
102
- }
103
-
104
- onAfterStart(handler: () => Promise<void> | void): this {
105
- this.hooks.onAfterStart = handler;
106
- return this;
107
- }
108
-
109
- onBeforeStop(handler: () => Promise<void> | void): this {
110
- this.hooks.onBeforeStop = handler;
111
- return this;
112
- }
113
-
114
- onAfterStop(handler: () => Promise<void> | void): this {
115
- this.hooks.onAfterStop = handler;
116
- return this;
117
- }
118
-
119
- onBeforeRestart(handler: () => Promise<void> | void): this {
120
- this.hooks.onBeforeRestart = handler;
121
- return this;
122
- }
123
-
124
- onAfterRestart(handler: () => Promise<void> | void): this {
125
- this.hooks.onAfterRestart = handler;
126
- return this;
127
- }
128
-
129
- onRequest(handler: (ctx: Context) => Promise<void> | void): this {
130
- this.hooks.onRequest = handler;
131
- return this;
132
- }
133
-
134
- onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
135
- this.hooks.onResponse = handler;
136
- return this;
137
- }
138
-
139
- onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
140
- this.hooks.onError = handler;
141
- return this;
142
- }
143
-
144
- // Plugin system - now accepts other BXO instances
145
- use(bxoInstance: BXO): this {
146
- this.plugins.push(bxoInstance);
147
- return this;
148
- }
149
-
150
- // HTTP method handlers with overloads for type safety
151
- get<TConfig extends RouteConfig = {}>(
152
- path: string,
153
- handler: Handler<TConfig>
154
- ): this;
155
- get<TConfig extends RouteConfig = {}>(
156
- path: string,
157
- handler: Handler<TConfig>,
158
- config: TConfig
159
- ): this;
160
- get<TConfig extends RouteConfig = {}>(
161
- path: string,
162
- handler: Handler<TConfig>,
163
- config?: TConfig
164
- ): this {
165
- this._routes.push({ method: 'GET', path, handler, config });
166
- return this;
167
- }
168
-
169
- post<TConfig extends RouteConfig = {}>(
170
- path: string,
171
- handler: Handler<TConfig>
172
- ): this;
173
- post<TConfig extends RouteConfig = {}>(
174
- path: string,
175
- handler: Handler<TConfig>,
176
- config: TConfig
177
- ): this;
178
- post<TConfig extends RouteConfig = {}>(
179
- path: string,
180
- handler: Handler<TConfig>,
181
- config?: TConfig
182
- ): this {
183
- this._routes.push({ method: 'POST', path, handler, config });
184
- return this;
185
- }
186
-
187
- put<TConfig extends RouteConfig = {}>(
188
- path: string,
189
- handler: Handler<TConfig>
190
- ): this;
191
- put<TConfig extends RouteConfig = {}>(
192
- path: string,
193
- handler: Handler<TConfig>,
194
- config: TConfig
195
- ): this;
196
- put<TConfig extends RouteConfig = {}>(
197
- path: string,
198
- handler: Handler<TConfig>,
199
- config?: TConfig
200
- ): this {
201
- this._routes.push({ method: 'PUT', path, handler, config });
202
- return this;
203
- }
204
-
205
- delete<TConfig extends RouteConfig = {}>(
206
- path: string,
207
- handler: Handler<TConfig>
208
- ): this;
209
- delete<TConfig extends RouteConfig = {}>(
210
- path: string,
211
- handler: Handler<TConfig>,
212
- config: TConfig
213
- ): this;
214
- delete<TConfig extends RouteConfig = {}>(
215
- path: string,
216
- handler: Handler<TConfig>,
217
- config?: TConfig
218
- ): this {
219
- this._routes.push({ method: 'DELETE', path, handler, config });
220
- return this;
221
- }
222
-
223
- patch<TConfig extends RouteConfig = {}>(
224
- path: string,
225
- handler: Handler<TConfig>
226
- ): this;
227
- patch<TConfig extends RouteConfig = {}>(
228
- path: string,
229
- handler: Handler<TConfig>,
230
- config: TConfig
231
- ): this;
232
- patch<TConfig extends RouteConfig = {}>(
233
- path: string,
234
- handler: Handler<TConfig>,
235
- config?: TConfig
236
- ): 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 });
244
- return this;
245
- }
246
-
247
- // Route matching utility
248
- private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
249
- for (const route of this._routes) {
250
- if (route.method !== method) continue;
251
-
252
- const routeSegments = route.path.split('/').filter(Boolean);
253
- const pathSegments = pathname.split('/').filter(Boolean);
254
-
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);
316
-
317
- const params: Record<string, string> = {};
318
- let isMatch = true;
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
-
331
- for (let i = 0; i < routeSegments.length; i++) {
332
- const routeSegment = routeSegments[i];
333
- const pathSegment = pathSegments[i];
334
-
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) {
349
- isMatch = false;
350
- break;
351
- }
352
-
353
- if (routeSegment.startsWith(':')) {
354
- const paramName = routeSegment.slice(1);
355
- params[paramName] = decodeURIComponent(pathSegment);
356
- } else if (routeSegment === '*') {
357
- // Single segment wildcard
358
- params['*'] = decodeURIComponent(pathSegment);
359
- } else if (routeSegment !== pathSegment) {
360
- isMatch = false;
361
- break;
362
- }
363
- }
364
-
365
- if (isMatch) {
366
- return { route, params };
367
- }
368
- }
369
-
370
- return null;
371
- }
372
-
373
- // Parse query string
374
- private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
375
- const query: Record<string, string | undefined> = {};
376
- for (const [key, value] of searchParams.entries()) {
377
- query[key] = value;
378
- }
379
- return query;
380
- }
381
-
382
- // Parse headers
383
- private parseHeaders(headers: Headers): Record<string, string> {
384
- const headerObj: Record<string, string> = {};
385
- for (const [key, value] of headers.entries()) {
386
- headerObj[key] = value;
387
- }
388
- return headerObj;
389
- }
390
-
391
- // Validate data against Zod schema
392
- private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
393
- if (!schema) return data;
394
- return schema.parse(data);
395
- }
396
-
397
- // Main request handler
398
- private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
399
- const url = new URL(request.url);
400
- const method = request.method;
401
- const pathname = url.pathname;
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
-
422
- const matchResult = this.matchRoute(method, pathname);
423
- if (!matchResult) {
424
- return new Response('Not Found', { status: 404 });
425
- }
426
-
427
- const { route, params } = matchResult;
428
- const query = this.parseQuery(url.searchParams);
429
- const headers = this.parseHeaders(request.headers);
430
-
431
- let body: any;
432
- if (request.method !== 'GET' && request.method !== 'HEAD') {
433
- const contentType = request.headers.get('content-type');
434
- if (contentType?.includes('application/json')) {
435
- try {
436
- body = await request.json();
437
- } catch {
438
- body = {};
439
- }
440
- } else if (contentType?.includes('application/x-www-form-urlencoded')) {
441
- const formData = await request.formData();
442
- body = Object.fromEntries(formData.entries());
443
- } else {
444
- body = await request.text();
445
- }
446
- }
447
-
448
- // Create context
449
- const ctx: Context = {
450
- params: route.config?.params ? this.validateData(route.config.params, params) : params,
451
- query: route.config?.query ? this.validateData(route.config.query, query) : query,
452
- body: route.config?.body ? this.validateData(route.config.body, body) : body,
453
- headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
454
- path: pathname,
455
- request,
456
- set: {}
457
- };
458
-
459
- try {
460
- // Run global onRequest hook
461
- if (this.hooks.onRequest) {
462
- await this.hooks.onRequest(ctx);
463
- }
464
-
465
- // Run BXO instance onRequest hooks
466
- for (const bxoInstance of this.plugins) {
467
- if (bxoInstance.hooks.onRequest) {
468
- await bxoInstance.hooks.onRequest(ctx);
469
- }
470
- }
471
-
472
- // Execute route handler
473
- let response = await route.handler(ctx);
474
-
475
- // Run global onResponse hook
476
- if (this.hooks.onResponse) {
477
- response = await this.hooks.onResponse(ctx, response) || response;
478
- }
479
-
480
- // Run BXO instance onResponse hooks
481
- for (const bxoInstance of this.plugins) {
482
- if (bxoInstance.hooks.onResponse) {
483
- response = await bxoInstance.hooks.onResponse(ctx, response) || 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
- });
498
- }
499
- }
500
-
501
- // Convert response to Response object
502
- if (response instanceof Response) {
503
- return response;
504
- }
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
-
535
- const responseInit: ResponseInit = {
536
- status: ctx.set.status || 200,
537
- headers: ctx.set.headers || {}
538
- };
539
-
540
- if (typeof response === 'string') {
541
- return new Response(response, responseInit);
542
- }
543
-
544
- return new Response(JSON.stringify(response), {
545
- ...responseInit,
546
- headers: {
547
- 'Content-Type': 'application/json',
548
- ...responseInit.headers
549
- }
550
- });
551
-
552
- } catch (error) {
553
- // Run error hooks
554
- let errorResponse: any;
555
-
556
- if (this.hooks.onError) {
557
- errorResponse = await this.hooks.onError(ctx, error as Error);
558
- }
559
-
560
- for (const bxoInstance of this.plugins) {
561
- if (bxoInstance.hooks.onError) {
562
- errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
563
- }
564
- }
565
-
566
- if (errorResponse) {
567
- if (errorResponse instanceof Response) {
568
- return errorResponse;
569
- }
570
- return new Response(JSON.stringify(errorResponse), {
571
- status: 500,
572
- headers: { 'Content-Type': 'application/json' }
573
- });
574
- }
575
-
576
- // Default error response
577
- const errorMessage = error instanceof Error ? error.message : 'Internal Server Error';
578
- return new Response(JSON.stringify({ error: errorMessage }), {
579
- status: 500,
580
- headers: { 'Content-Type': 'application/json' }
581
- });
582
- }
583
- }
584
-
585
- // Hot reload functionality
586
- enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
587
- this.hotReloadEnabled = true;
588
- watchPaths.forEach(path => this.watchedFiles.add(path));
589
- excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
590
- return this;
591
- }
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
-
629
- private async setupFileWatcher(port: number, hostname: string): Promise<void> {
630
- if (!this.hotReloadEnabled) return;
631
-
632
- const fs = require('fs');
633
-
634
- for (const watchPath of this.watchedFiles) {
635
- try {
636
- fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
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
-
643
- console.log(`🔄 File changed: ${filename}, restarting server...`);
644
- await this.restart(port, hostname);
645
- }
646
- });
647
- console.log(`👀 Watching ${watchPath} for changes...`);
648
- if (this.watchedExclude.size > 0) {
649
- console.log(`🚫 Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
650
- }
651
- } catch (error) {
652
- console.warn(`⚠️ Could not watch ${watchPath}:`, error);
653
- }
654
- }
655
- }
656
-
657
- // Server management methods
658
- async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
659
- if (this.isRunning) {
660
- console.log('⚠️ Server is already running');
661
- return;
662
- }
663
-
664
- try {
665
- // Before start hook
666
- if (this.hooks.onBeforeStart) {
667
- await this.hooks.onBeforeStart();
668
- }
669
-
670
- this.server = Bun.serve({
671
- port,
672
- hostname,
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
- }
694
- });
695
-
696
- this.isRunning = true;
697
- this.serverPort = port;
698
- this.serverHostname = hostname;
699
-
700
- console.log(`🦊 BXO server running at http://${hostname}:${port}`);
701
-
702
- // After start hook
703
- if (this.hooks.onAfterStart) {
704
- await this.hooks.onAfterStart();
705
- }
706
-
707
- // Setup hot reload
708
- await this.setupFileWatcher(port, hostname);
709
-
710
- // Handle graceful shutdown
711
- const shutdownHandler = async () => {
712
- await this.stop();
713
- process.exit(0);
714
- };
715
-
716
- process.on('SIGINT', shutdownHandler);
717
- process.on('SIGTERM', shutdownHandler);
718
-
719
- } catch (error) {
720
- console.error('❌ Failed to start server:', error);
721
- throw error;
722
- }
723
- }
724
-
725
- async stop(): Promise<void> {
726
- if (!this.isRunning) {
727
- console.log('⚠️ Server is not running');
728
- return;
729
- }
730
-
731
- try {
732
- // Before stop hook
733
- if (this.hooks.onBeforeStop) {
734
- await this.hooks.onBeforeStop();
735
- }
736
-
737
- if (this.server) {
738
- this.server.stop();
739
- this.server = null;
740
- }
741
-
742
- this.isRunning = false;
743
- this.serverPort = undefined;
744
- this.serverHostname = undefined;
745
-
746
- console.log('🛑 BXO server stopped');
747
-
748
- // After stop hook
749
- if (this.hooks.onAfterStop) {
750
- await this.hooks.onAfterStop();
751
- }
752
-
753
- } catch (error) {
754
- console.error('❌ Error stopping server:', error);
755
- throw error;
756
- }
757
- }
758
-
759
- async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
760
- try {
761
- // Before restart hook
762
- if (this.hooks.onBeforeRestart) {
763
- await this.hooks.onBeforeRestart();
764
- }
765
-
766
- console.log('🔄 Restarting BXO server...');
767
-
768
- await this.stop();
769
-
770
- // Small delay to ensure cleanup
771
- await new Promise(resolve => setTimeout(resolve, 100));
772
-
773
- await this.start(port, hostname);
774
-
775
- // After restart hook
776
- if (this.hooks.onAfterRestart) {
777
- await this.hooks.onAfterRestart();
778
- }
779
-
780
- } catch (error) {
781
- console.error('❌ Error restarting server:', error);
782
- throw error;
783
- }
784
- }
785
-
786
- // Backward compatibility
787
- async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
788
- return this.start(port, hostname);
789
- }
790
-
791
- // Server status
792
- isServerRunning(): boolean {
793
- return this.isRunning;
794
- }
795
-
796
- getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
797
- return {
798
- running: this.isRunning,
799
- hotReload: this.hotReloadEnabled,
800
- watchedFiles: Array.from(this.watchedFiles),
801
- excludePatterns: Array.from(this.watchedExclude)
802
- };
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 });
863
- }
864
-
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;
879
- }
880
-
881
- // Export Zod for convenience
882
- export { z, error, file };
883
-
884
- // Export types for external use
885
- export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };