bxo 0.0.5-dev.6 → 0.0.5-dev.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -2
- package/index.ts +4 -787
- package/package.json +11 -5
- package/plugins/README.md +160 -0
- package/plugins/cors.ts +81 -57
- package/plugins/index.ts +4 -6
- package/plugins/ratelimit.ts +55 -59
- package/src/core/bxo.ts +438 -0
- package/src/handlers/request-handler.ts +229 -0
- package/src/index.ts +59 -0
- package/src/types/index.ts +164 -0
- package/src/utils/context-factory.ts +158 -0
- package/src/utils/helpers.ts +40 -0
- package/src/utils/index.ts +377 -0
- package/src/utils/response-handler.ts +286 -0
- package/src/utils/route-matcher.ts +191 -0
- package/tests/README.md +359 -0
- package/tests/integration/bxo.test.ts +598 -0
- package/tests/run-tests.ts +44 -0
- package/tests/unit/context-factory.test.ts +386 -0
- package/tests/unit/helpers.test.ts +253 -0
- package/tests/unit/response-handler.test.ts +301 -0
- package/tests/unit/route-matcher.test.ts +181 -0
- package/tests/unit/utils.test.ts +433 -0
- package/example.ts +0 -183
- package/plugins/auth.ts +0 -119
- package/plugins/logger.ts +0 -109
package/index.ts
CHANGED
|
@@ -1,788 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Re-export everything from the refactored source
|
|
2
|
+
export * from './src/index';
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
|
|
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
|
-
request: Request;
|
|
35
|
-
set: {
|
|
36
|
-
status?: number;
|
|
37
|
-
headers?: Record<string, string>;
|
|
38
|
-
};
|
|
39
|
-
// Extended properties that can be added by plugins
|
|
40
|
-
user?: any;
|
|
41
|
-
[key: string]: any;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Handler function type
|
|
45
|
-
type Handler<TConfig extends RouteConfig = {}> = (ctx: Context<TConfig>) => Promise<any> | any;
|
|
46
|
-
|
|
47
|
-
// Route definition
|
|
48
|
-
interface Route {
|
|
49
|
-
method: string;
|
|
50
|
-
path: string;
|
|
51
|
-
handler: Handler<any>;
|
|
52
|
-
config?: RouteConfig;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// WebSocket handler interface
|
|
56
|
-
interface WebSocketHandler {
|
|
57
|
-
onOpen?: (ws: any) => void;
|
|
58
|
-
onMessage?: (ws: any, message: string | Buffer) => void;
|
|
59
|
-
onClose?: (ws: any, code?: number, reason?: string) => void;
|
|
60
|
-
onError?: (ws: any, error: Error) => void;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// WebSocket route definition
|
|
64
|
-
interface WSRoute {
|
|
65
|
-
path: string;
|
|
66
|
-
handler: WebSocketHandler;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Lifecycle hooks
|
|
70
|
-
interface LifecycleHooks {
|
|
71
|
-
onBeforeStart?: () => Promise<void> | void;
|
|
72
|
-
onAfterStart?: () => Promise<void> | void;
|
|
73
|
-
onBeforeStop?: () => Promise<void> | void;
|
|
74
|
-
onAfterStop?: () => Promise<void> | void;
|
|
75
|
-
onBeforeRestart?: () => Promise<void> | void;
|
|
76
|
-
onAfterRestart?: () => Promise<void> | void;
|
|
77
|
-
onRequest?: (ctx: Context) => Promise<void> | void;
|
|
78
|
-
onResponse?: (ctx: Context, response: any) => Promise<any> | any;
|
|
79
|
-
onError?: (ctx: Context, error: Error) => Promise<any> | any;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export default class BXO {
|
|
83
|
-
private _routes: Route[] = [];
|
|
84
|
-
private _wsRoutes: WSRoute[] = [];
|
|
85
|
-
private plugins: BXO[] = [];
|
|
86
|
-
private hooks: LifecycleHooks = {};
|
|
87
|
-
private server?: any;
|
|
88
|
-
private isRunning: boolean = false;
|
|
89
|
-
private hotReloadEnabled: boolean = false;
|
|
90
|
-
private watchedFiles: Set<string> = new Set();
|
|
91
|
-
private watchedExclude: Set<string> = new Set();
|
|
92
|
-
private serverPort?: number;
|
|
93
|
-
private serverHostname?: string;
|
|
94
|
-
|
|
95
|
-
constructor() { }
|
|
96
|
-
|
|
97
|
-
// Lifecycle hook methods
|
|
98
|
-
onBeforeStart(handler: () => Promise<void> | void): this {
|
|
99
|
-
this.hooks.onBeforeStart = handler;
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
onAfterStart(handler: () => Promise<void> | void): this {
|
|
104
|
-
this.hooks.onAfterStart = handler;
|
|
105
|
-
return this;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
onBeforeStop(handler: () => Promise<void> | void): this {
|
|
109
|
-
this.hooks.onBeforeStop = handler;
|
|
110
|
-
return this;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
onAfterStop(handler: () => Promise<void> | void): this {
|
|
114
|
-
this.hooks.onAfterStop = handler;
|
|
115
|
-
return this;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
onBeforeRestart(handler: () => Promise<void> | void): this {
|
|
119
|
-
this.hooks.onBeforeRestart = handler;
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
onAfterRestart(handler: () => Promise<void> | void): this {
|
|
124
|
-
this.hooks.onAfterRestart = handler;
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
onRequest(handler: (ctx: Context) => Promise<void> | void): this {
|
|
129
|
-
this.hooks.onRequest = handler;
|
|
130
|
-
return this;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
onResponse(handler: (ctx: Context, response: any) => Promise<any> | any): this {
|
|
134
|
-
this.hooks.onResponse = handler;
|
|
135
|
-
return this;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
onError(handler: (ctx: Context, error: Error) => Promise<any> | any): this {
|
|
139
|
-
this.hooks.onError = handler;
|
|
140
|
-
return this;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Plugin system - now accepts other BXO instances
|
|
144
|
-
use(bxoInstance: BXO): this {
|
|
145
|
-
this.plugins.push(bxoInstance);
|
|
146
|
-
return this;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// HTTP method handlers with overloads for type safety
|
|
150
|
-
get<TConfig extends RouteConfig = {}>(
|
|
151
|
-
path: string,
|
|
152
|
-
handler: Handler<TConfig>
|
|
153
|
-
): this;
|
|
154
|
-
get<TConfig extends RouteConfig = {}>(
|
|
155
|
-
path: string,
|
|
156
|
-
handler: Handler<TConfig>,
|
|
157
|
-
config: TConfig
|
|
158
|
-
): this;
|
|
159
|
-
get<TConfig extends RouteConfig = {}>(
|
|
160
|
-
path: string,
|
|
161
|
-
handler: Handler<TConfig>,
|
|
162
|
-
config?: TConfig
|
|
163
|
-
): this {
|
|
164
|
-
this._routes.push({ method: 'GET', path, handler, config });
|
|
165
|
-
return this;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
post<TConfig extends RouteConfig = {}>(
|
|
169
|
-
path: string,
|
|
170
|
-
handler: Handler<TConfig>
|
|
171
|
-
): this;
|
|
172
|
-
post<TConfig extends RouteConfig = {}>(
|
|
173
|
-
path: string,
|
|
174
|
-
handler: Handler<TConfig>,
|
|
175
|
-
config: TConfig
|
|
176
|
-
): this;
|
|
177
|
-
post<TConfig extends RouteConfig = {}>(
|
|
178
|
-
path: string,
|
|
179
|
-
handler: Handler<TConfig>,
|
|
180
|
-
config?: TConfig
|
|
181
|
-
): this {
|
|
182
|
-
this._routes.push({ method: 'POST', path, handler, config });
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
put<TConfig extends RouteConfig = {}>(
|
|
187
|
-
path: string,
|
|
188
|
-
handler: Handler<TConfig>
|
|
189
|
-
): this;
|
|
190
|
-
put<TConfig extends RouteConfig = {}>(
|
|
191
|
-
path: string,
|
|
192
|
-
handler: Handler<TConfig>,
|
|
193
|
-
config: TConfig
|
|
194
|
-
): this;
|
|
195
|
-
put<TConfig extends RouteConfig = {}>(
|
|
196
|
-
path: string,
|
|
197
|
-
handler: Handler<TConfig>,
|
|
198
|
-
config?: TConfig
|
|
199
|
-
): this {
|
|
200
|
-
this._routes.push({ method: 'PUT', path, handler, config });
|
|
201
|
-
return this;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
delete<TConfig extends RouteConfig = {}>(
|
|
205
|
-
path: string,
|
|
206
|
-
handler: Handler<TConfig>
|
|
207
|
-
): this;
|
|
208
|
-
delete<TConfig extends RouteConfig = {}>(
|
|
209
|
-
path: string,
|
|
210
|
-
handler: Handler<TConfig>,
|
|
211
|
-
config: TConfig
|
|
212
|
-
): this;
|
|
213
|
-
delete<TConfig extends RouteConfig = {}>(
|
|
214
|
-
path: string,
|
|
215
|
-
handler: Handler<TConfig>,
|
|
216
|
-
config?: TConfig
|
|
217
|
-
): this {
|
|
218
|
-
this._routes.push({ method: 'DELETE', path, handler, config });
|
|
219
|
-
return this;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
patch<TConfig extends RouteConfig = {}>(
|
|
223
|
-
path: string,
|
|
224
|
-
handler: Handler<TConfig>
|
|
225
|
-
): this;
|
|
226
|
-
patch<TConfig extends RouteConfig = {}>(
|
|
227
|
-
path: string,
|
|
228
|
-
handler: Handler<TConfig>,
|
|
229
|
-
config: TConfig
|
|
230
|
-
): this;
|
|
231
|
-
patch<TConfig extends RouteConfig = {}>(
|
|
232
|
-
path: string,
|
|
233
|
-
handler: Handler<TConfig>,
|
|
234
|
-
config?: TConfig
|
|
235
|
-
): this {
|
|
236
|
-
this._routes.push({ method: 'PATCH', path, handler, config });
|
|
237
|
-
return this;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// WebSocket route handler
|
|
241
|
-
ws(path: string, handler: WebSocketHandler): this {
|
|
242
|
-
this._wsRoutes.push({ path, handler });
|
|
243
|
-
return this;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Route matching utility
|
|
247
|
-
private matchRoute(method: string, pathname: string): { route: Route; params: Record<string, string> } | null {
|
|
248
|
-
for (const route of this._routes) {
|
|
249
|
-
if (route.method !== method) continue;
|
|
250
|
-
|
|
251
|
-
const routeSegments = route.path.split('/').filter(Boolean);
|
|
252
|
-
const pathSegments = pathname.split('/').filter(Boolean);
|
|
253
|
-
|
|
254
|
-
if (routeSegments.length !== pathSegments.length) continue;
|
|
255
|
-
|
|
256
|
-
const params: Record<string, string> = {};
|
|
257
|
-
let isMatch = true;
|
|
258
|
-
|
|
259
|
-
for (let i = 0; i < routeSegments.length; i++) {
|
|
260
|
-
const routeSegment = routeSegments[i];
|
|
261
|
-
const pathSegment = pathSegments[i];
|
|
262
|
-
|
|
263
|
-
if (!routeSegment || !pathSegment) {
|
|
264
|
-
isMatch = false;
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (routeSegment.startsWith(':')) {
|
|
269
|
-
const paramName = routeSegment.slice(1);
|
|
270
|
-
params[paramName] = decodeURIComponent(pathSegment);
|
|
271
|
-
} else if (routeSegment !== pathSegment) {
|
|
272
|
-
isMatch = false;
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (isMatch) {
|
|
278
|
-
return { route, params };
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// WebSocket route matching utility
|
|
286
|
-
private matchWSRoute(pathname: string): { route: WSRoute; params: Record<string, string> } | null {
|
|
287
|
-
for (const route of this._wsRoutes) {
|
|
288
|
-
const routeSegments = route.path.split('/').filter(Boolean);
|
|
289
|
-
const pathSegments = pathname.split('/').filter(Boolean);
|
|
290
|
-
|
|
291
|
-
if (routeSegments.length !== pathSegments.length) continue;
|
|
292
|
-
|
|
293
|
-
const params: Record<string, string> = {};
|
|
294
|
-
let isMatch = true;
|
|
295
|
-
|
|
296
|
-
for (let i = 0; i < routeSegments.length; i++) {
|
|
297
|
-
const routeSegment = routeSegments[i];
|
|
298
|
-
const pathSegment = pathSegments[i];
|
|
299
|
-
|
|
300
|
-
if (!routeSegment || !pathSegment) {
|
|
301
|
-
isMatch = false;
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (routeSegment.startsWith(':')) {
|
|
306
|
-
const paramName = routeSegment.slice(1);
|
|
307
|
-
params[paramName] = decodeURIComponent(pathSegment);
|
|
308
|
-
} else if (routeSegment !== pathSegment) {
|
|
309
|
-
isMatch = false;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (isMatch) {
|
|
315
|
-
return { route, params };
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Parse query string
|
|
323
|
-
private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
|
|
324
|
-
const query: Record<string, string | undefined> = {};
|
|
325
|
-
for (const [key, value] of searchParams.entries()) {
|
|
326
|
-
query[key] = value;
|
|
327
|
-
}
|
|
328
|
-
return query;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Parse headers
|
|
332
|
-
private parseHeaders(headers: Headers): Record<string, string> {
|
|
333
|
-
const headerObj: Record<string, string> = {};
|
|
334
|
-
for (const [key, value] of headers.entries()) {
|
|
335
|
-
headerObj[key] = value;
|
|
336
|
-
}
|
|
337
|
-
return headerObj;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Validate data against Zod schema
|
|
341
|
-
private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
|
|
342
|
-
if (!schema) return data;
|
|
343
|
-
return schema.parse(data);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Main request handler
|
|
347
|
-
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
|
348
|
-
const url = new URL(request.url);
|
|
349
|
-
const method = request.method;
|
|
350
|
-
const pathname = url.pathname;
|
|
351
|
-
|
|
352
|
-
// Check for WebSocket upgrade
|
|
353
|
-
if (request.headers.get('upgrade') === 'websocket') {
|
|
354
|
-
const wsMatchResult = this.matchWSRoute(pathname);
|
|
355
|
-
if (wsMatchResult && server) {
|
|
356
|
-
const success = server.upgrade(request, {
|
|
357
|
-
data: {
|
|
358
|
-
handler: wsMatchResult.route.handler,
|
|
359
|
-
params: wsMatchResult.params,
|
|
360
|
-
pathname
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (success) {
|
|
365
|
-
return; // undefined response means upgrade was successful
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const matchResult = this.matchRoute(method, pathname);
|
|
372
|
-
if (!matchResult) {
|
|
373
|
-
return new Response('Not Found', { status: 404 });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const { route, params } = matchResult;
|
|
377
|
-
const query = this.parseQuery(url.searchParams);
|
|
378
|
-
const headers = this.parseHeaders(request.headers);
|
|
379
|
-
|
|
380
|
-
let body: any;
|
|
381
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
382
|
-
const contentType = request.headers.get('content-type');
|
|
383
|
-
if (contentType?.includes('application/json')) {
|
|
384
|
-
try {
|
|
385
|
-
body = await request.json();
|
|
386
|
-
} catch {
|
|
387
|
-
body = {};
|
|
388
|
-
}
|
|
389
|
-
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
390
|
-
const formData = await request.formData();
|
|
391
|
-
body = Object.fromEntries(formData.entries());
|
|
392
|
-
} else {
|
|
393
|
-
body = await request.text();
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Create context
|
|
398
|
-
const ctx: Context = {
|
|
399
|
-
params: route.config?.params ? this.validateData(route.config.params, params) : params,
|
|
400
|
-
query: route.config?.query ? this.validateData(route.config.query, query) : query,
|
|
401
|
-
body: route.config?.body ? this.validateData(route.config.body, body) : body,
|
|
402
|
-
headers: route.config?.headers ? this.validateData(route.config.headers, headers) : headers,
|
|
403
|
-
request,
|
|
404
|
-
set: {}
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
try {
|
|
408
|
-
// Run global onRequest hook
|
|
409
|
-
if (this.hooks.onRequest) {
|
|
410
|
-
await this.hooks.onRequest(ctx);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Run BXO instance onRequest hooks
|
|
414
|
-
for (const bxoInstance of this.plugins) {
|
|
415
|
-
if (bxoInstance.hooks.onRequest) {
|
|
416
|
-
await bxoInstance.hooks.onRequest(ctx);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Execute route handler
|
|
421
|
-
let response = await route.handler(ctx);
|
|
422
|
-
|
|
423
|
-
// Run global onResponse hook
|
|
424
|
-
if (this.hooks.onResponse) {
|
|
425
|
-
response = await this.hooks.onResponse(ctx, response) || response;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Run BXO instance onResponse hooks
|
|
429
|
-
for (const bxoInstance of this.plugins) {
|
|
430
|
-
if (bxoInstance.hooks.onResponse) {
|
|
431
|
-
response = await bxoInstance.hooks.onResponse(ctx, response) || response;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Validate response against schema if provided
|
|
436
|
-
if (route.config?.response && !(response instanceof Response)) {
|
|
437
|
-
try {
|
|
438
|
-
response = this.validateData(route.config.response, response);
|
|
439
|
-
} catch (validationError) {
|
|
440
|
-
// Response validation failed
|
|
441
|
-
const errorMessage = validationError instanceof Error ? validationError.message : 'Response validation failed';
|
|
442
|
-
return new Response(JSON.stringify({ error: `Response validation error: ${errorMessage}` }), {
|
|
443
|
-
status: 500,
|
|
444
|
-
headers: { 'Content-Type': 'application/json' }
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Convert response to Response object
|
|
450
|
-
if (response instanceof Response) {
|
|
451
|
-
return response;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const responseInit: ResponseInit = {
|
|
455
|
-
status: ctx.set.status || 200,
|
|
456
|
-
headers: ctx.set.headers || {}
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
if (typeof response === 'string') {
|
|
460
|
-
return new Response(response, responseInit);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return new Response(JSON.stringify(response), {
|
|
464
|
-
...responseInit,
|
|
465
|
-
headers: {
|
|
466
|
-
'Content-Type': 'application/json',
|
|
467
|
-
...responseInit.headers
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
} catch (error) {
|
|
472
|
-
// Run error hooks
|
|
473
|
-
let errorResponse: any;
|
|
474
|
-
|
|
475
|
-
if (this.hooks.onError) {
|
|
476
|
-
errorResponse = await this.hooks.onError(ctx, error as Error);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
for (const bxoInstance of this.plugins) {
|
|
480
|
-
if (bxoInstance.hooks.onError) {
|
|
481
|
-
errorResponse = await bxoInstance.hooks.onError(ctx, error as Error) || errorResponse;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (errorResponse) {
|
|
486
|
-
if (errorResponse instanceof Response) {
|
|
487
|
-
return errorResponse;
|
|
488
|
-
}
|
|
489
|
-
return new Response(JSON.stringify(errorResponse), {
|
|
490
|
-
status: 500,
|
|
491
|
-
headers: { 'Content-Type': 'application/json' }
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Default error response
|
|
496
|
-
const errorMessage = error instanceof Error ? error.message : 'Internal Server Error';
|
|
497
|
-
return new Response(JSON.stringify({ error: errorMessage }), {
|
|
498
|
-
status: 500,
|
|
499
|
-
headers: { 'Content-Type': 'application/json' }
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Hot reload functionality
|
|
505
|
-
enableHotReload(watchPaths: string[] = ['./'], excludePatterns: string[] = []): this {
|
|
506
|
-
this.hotReloadEnabled = true;
|
|
507
|
-
watchPaths.forEach(path => this.watchedFiles.add(path));
|
|
508
|
-
excludePatterns.forEach(pattern => this.watchedExclude.add(pattern));
|
|
509
|
-
return this;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
private shouldExcludeFile(filename: string): boolean {
|
|
513
|
-
for (const pattern of this.watchedExclude) {
|
|
514
|
-
// Handle exact match
|
|
515
|
-
if (pattern === filename) {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Handle directory patterns (e.g., "node_modules/", "dist/")
|
|
520
|
-
if (pattern.endsWith('/')) {
|
|
521
|
-
if (filename.startsWith(pattern) || filename.includes(`/${pattern}`)) {
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Handle wildcard patterns (e.g., "*.log", "temp*")
|
|
527
|
-
if (pattern.includes('*')) {
|
|
528
|
-
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
529
|
-
if (regex.test(filename)) {
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Handle file extension patterns (e.g., ".log", ".tmp")
|
|
535
|
-
if (pattern.startsWith('.') && filename.endsWith(pattern)) {
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Handle substring matches for directories
|
|
540
|
-
if (filename.includes(pattern)) {
|
|
541
|
-
return true;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
private async setupFileWatcher(port: number, hostname: string): Promise<void> {
|
|
549
|
-
if (!this.hotReloadEnabled) return;
|
|
550
|
-
|
|
551
|
-
const fs = require('fs');
|
|
552
|
-
|
|
553
|
-
for (const watchPath of this.watchedFiles) {
|
|
554
|
-
try {
|
|
555
|
-
fs.watch(watchPath, { recursive: true }, async (eventType: string, filename: string) => {
|
|
556
|
-
if (filename && (filename.endsWith('.ts') || filename.endsWith('.js'))) {
|
|
557
|
-
// Check if file should be excluded
|
|
558
|
-
if (this.shouldExcludeFile(filename)) {
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
console.log(`🔄 File changed: ${filename}, restarting server...`);
|
|
563
|
-
await this.restart(port, hostname);
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
console.log(`👀 Watching ${watchPath} for changes...`);
|
|
567
|
-
if (this.watchedExclude.size > 0) {
|
|
568
|
-
console.log(`🚫 Excluding patterns: ${Array.from(this.watchedExclude).join(', ')}`);
|
|
569
|
-
}
|
|
570
|
-
} catch (error) {
|
|
571
|
-
console.warn(`⚠️ Could not watch ${watchPath}:`, error);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Server management methods
|
|
577
|
-
async start(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
|
578
|
-
if (this.isRunning) {
|
|
579
|
-
console.log('⚠️ Server is already running');
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
// Before start hook
|
|
585
|
-
if (this.hooks.onBeforeStart) {
|
|
586
|
-
await this.hooks.onBeforeStart();
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
this.server = Bun.serve({
|
|
590
|
-
port,
|
|
591
|
-
hostname,
|
|
592
|
-
fetch: (request, server) => this.handleRequest(request, server),
|
|
593
|
-
websocket: {
|
|
594
|
-
message: (ws: any, message: any) => {
|
|
595
|
-
const handler = ws.data?.handler;
|
|
596
|
-
if (handler?.onMessage) {
|
|
597
|
-
handler.onMessage(ws, message);
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
open: (ws: any) => {
|
|
601
|
-
const handler = ws.data?.handler;
|
|
602
|
-
if (handler?.onOpen) {
|
|
603
|
-
handler.onOpen(ws);
|
|
604
|
-
}
|
|
605
|
-
},
|
|
606
|
-
close: (ws: any, code?: number, reason?: string) => {
|
|
607
|
-
const handler = ws.data?.handler;
|
|
608
|
-
if (handler?.onClose) {
|
|
609
|
-
handler.onClose(ws, code, reason);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
this.isRunning = true;
|
|
616
|
-
this.serverPort = port;
|
|
617
|
-
this.serverHostname = hostname;
|
|
618
|
-
|
|
619
|
-
console.log(`🦊 BXO server running at http://${hostname}:${port}`);
|
|
620
|
-
|
|
621
|
-
// After start hook
|
|
622
|
-
if (this.hooks.onAfterStart) {
|
|
623
|
-
await this.hooks.onAfterStart();
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Setup hot reload
|
|
627
|
-
await this.setupFileWatcher(port, hostname);
|
|
628
|
-
|
|
629
|
-
// Handle graceful shutdown
|
|
630
|
-
const shutdownHandler = async () => {
|
|
631
|
-
await this.stop();
|
|
632
|
-
process.exit(0);
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
process.on('SIGINT', shutdownHandler);
|
|
636
|
-
process.on('SIGTERM', shutdownHandler);
|
|
637
|
-
|
|
638
|
-
} catch (error) {
|
|
639
|
-
console.error('❌ Failed to start server:', error);
|
|
640
|
-
throw error;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async stop(): Promise<void> {
|
|
645
|
-
if (!this.isRunning) {
|
|
646
|
-
console.log('⚠️ Server is not running');
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
// Before stop hook
|
|
652
|
-
if (this.hooks.onBeforeStop) {
|
|
653
|
-
await this.hooks.onBeforeStop();
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (this.server) {
|
|
657
|
-
this.server.stop();
|
|
658
|
-
this.server = null;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
this.isRunning = false;
|
|
662
|
-
this.serverPort = undefined;
|
|
663
|
-
this.serverHostname = undefined;
|
|
664
|
-
|
|
665
|
-
console.log('🛑 BXO server stopped');
|
|
666
|
-
|
|
667
|
-
// After stop hook
|
|
668
|
-
if (this.hooks.onAfterStop) {
|
|
669
|
-
await this.hooks.onAfterStop();
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
} catch (error) {
|
|
673
|
-
console.error('❌ Error stopping server:', error);
|
|
674
|
-
throw error;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
async restart(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
|
679
|
-
try {
|
|
680
|
-
// Before restart hook
|
|
681
|
-
if (this.hooks.onBeforeRestart) {
|
|
682
|
-
await this.hooks.onBeforeRestart();
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
console.log('🔄 Restarting BXO server...');
|
|
686
|
-
|
|
687
|
-
await this.stop();
|
|
688
|
-
|
|
689
|
-
// Small delay to ensure cleanup
|
|
690
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
691
|
-
|
|
692
|
-
await this.start(port, hostname);
|
|
693
|
-
|
|
694
|
-
// After restart hook
|
|
695
|
-
if (this.hooks.onAfterRestart) {
|
|
696
|
-
await this.hooks.onAfterRestart();
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
} catch (error) {
|
|
700
|
-
console.error('❌ Error restarting server:', error);
|
|
701
|
-
throw error;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Backward compatibility
|
|
706
|
-
async listen(port: number = 3000, hostname: string = 'localhost'): Promise<void> {
|
|
707
|
-
return this.start(port, hostname);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Server status
|
|
711
|
-
isServerRunning(): boolean {
|
|
712
|
-
return this.isRunning;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
getServerInfo(): { running: boolean; hotReload: boolean; watchedFiles: string[]; excludePatterns: string[] } {
|
|
716
|
-
return {
|
|
717
|
-
running: this.isRunning,
|
|
718
|
-
hotReload: this.hotReloadEnabled,
|
|
719
|
-
watchedFiles: Array.from(this.watchedFiles),
|
|
720
|
-
excludePatterns: Array.from(this.watchedExclude)
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Get server information (alias for getServerInfo)
|
|
725
|
-
get info() {
|
|
726
|
-
return {
|
|
727
|
-
// Server status
|
|
728
|
-
running: this.isRunning,
|
|
729
|
-
server: this.server ? 'Bun' : null,
|
|
730
|
-
|
|
731
|
-
// Connection details
|
|
732
|
-
hostname: this.serverHostname,
|
|
733
|
-
port: this.serverPort,
|
|
734
|
-
url: this.isRunning && this.serverHostname && this.serverPort
|
|
735
|
-
? `http://${this.serverHostname}:${this.serverPort}`
|
|
736
|
-
: null,
|
|
737
|
-
|
|
738
|
-
// Application statistics
|
|
739
|
-
totalRoutes: this._routes.length,
|
|
740
|
-
totalWsRoutes: this._wsRoutes.length,
|
|
741
|
-
totalPlugins: this.plugins.length,
|
|
742
|
-
|
|
743
|
-
// Hot reload configuration
|
|
744
|
-
hotReload: this.hotReloadEnabled,
|
|
745
|
-
watchedFiles: Array.from(this.watchedFiles),
|
|
746
|
-
excludePatterns: Array.from(this.watchedExclude),
|
|
747
|
-
|
|
748
|
-
// System information
|
|
749
|
-
runtime: 'Bun',
|
|
750
|
-
version: typeof Bun !== 'undefined' ? Bun.version : 'unknown',
|
|
751
|
-
pid: process.pid,
|
|
752
|
-
uptime: this.isRunning ? process.uptime() : 0
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Get all routes information
|
|
757
|
-
get routes() {
|
|
758
|
-
return this._routes.map((route: Route) => ({
|
|
759
|
-
method: route.method,
|
|
760
|
-
path: route.path,
|
|
761
|
-
hasConfig: !!route.config,
|
|
762
|
-
config: route.config || null
|
|
763
|
-
}));
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Get all WebSocket routes information
|
|
767
|
-
get wsRoutes() {
|
|
768
|
-
return this._wsRoutes.map((route: WSRoute) => ({
|
|
769
|
-
path: route.path,
|
|
770
|
-
hasHandlers: {
|
|
771
|
-
onOpen: !!route.handler.onOpen,
|
|
772
|
-
onMessage: !!route.handler.onMessage,
|
|
773
|
-
onClose: !!route.handler.onClose,
|
|
774
|
-
onError: !!route.handler.onError
|
|
775
|
-
}
|
|
776
|
-
}));
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const error = (error: Error | string, status: number = 500) => {
|
|
781
|
-
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), { status });
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Export Zod for convenience
|
|
785
|
-
export { z, error };
|
|
786
|
-
|
|
787
|
-
// Export types for external use
|
|
788
|
-
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
|
|
4
|
+
// Also export the default BXO class for backward compatibility
|
|
5
|
+
export { default } from './src/index';
|