bxo 0.0.5-dev.38 → 0.0.5-dev.39

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 CHANGED
@@ -203,7 +203,7 @@ app.post('/api/users/:id', async (ctx) => {
203
203
 
204
204
  ## 🔌 Plugin System
205
205
 
206
- BXO has a powerful plugin system with lifecycle hooks. Plugins are separate modules imported from `./plugins`.
206
+ BXO has a powerful plugin system with lifecycle hooks. Plugins can be either middleware-style plugins or full BXO instances. Middleware plugins are separate modules imported from `./plugins` and are executed in the order they are added.
207
207
 
208
208
  ### Available Plugins
209
209
 
@@ -275,6 +275,8 @@ app.use(rateLimit({
275
275
 
276
276
  ### Creating Custom Plugins
277
277
 
278
+ #### Middleware-Style Plugins (Recommended)
279
+
278
280
  ```typescript
279
281
  const customPlugin = {
280
282
  name: 'custom',
@@ -295,6 +297,19 @@ const customPlugin = {
295
297
  app.use(customPlugin);
296
298
  ```
297
299
 
300
+ #### Full BXO Instance Plugins
301
+
302
+ You can also use full BXO instances as plugins, which will merge their routes and hooks:
303
+
304
+ ```typescript
305
+ const pluginApp = new BXO();
306
+ pluginApp.get('/plugin-route', async (ctx) => {
307
+ return { message: 'From plugin' };
308
+ });
309
+
310
+ app.use(pluginApp);
311
+ ```
312
+
298
313
  ## 🎣 Lifecycle Hooks
299
314
 
300
315
  BXO provides comprehensive lifecycle hooks with a consistent before/after pattern for both server and request lifecycle:
package/index.ts CHANGED
@@ -133,11 +133,30 @@ interface BXOOptions {
133
133
  enableValidation?: boolean;
134
134
  }
135
135
 
136
+ // Plugin interface for middleware-style plugins
137
+ interface Plugin {
138
+ name?: string;
139
+ onRequest?: (ctx: Context) => Promise<void> | void;
140
+ onResponse?: (ctx: Context, response: any) => Promise<any> | any;
141
+ onError?: (ctx: Context, error: Error) => Promise<any> | any;
142
+ }
143
+
136
144
  export default class BXO {
137
145
  private _routes: Route[] = [];
138
146
  private _wsRoutes: WSRoute[] = [];
139
147
  private plugins: BXO[] = [];
140
- private hooks: LifecycleHooks = {};
148
+ private middleware: Plugin[] = []; // New middleware array
149
+ private hooks: {
150
+ onBeforeStart?: (instance: BXO) => Promise<void> | void;
151
+ onAfterStart?: (instance: BXO) => Promise<void> | void;
152
+ onBeforeRestart?: (instance: BXO) => Promise<void> | void;
153
+ onAfterRestart?: (instance: BXO) => Promise<void> | void;
154
+ onBeforeStop?: (instance: BXO) => Promise<void> | void;
155
+ onAfterStop?: (instance: BXO) => Promise<void> | void;
156
+ onRequest?: (ctx: Context, instance: BXO) => Promise<void> | void;
157
+ onResponse?: (ctx: Context, response: any, instance: BXO) => Promise<any> | any;
158
+ onError?: (ctx: Context, error: Error, instance: BXO) => Promise<any> | any;
159
+ } = {};
141
160
  private server?: any;
142
161
  private isRunning: boolean = false;
143
162
  private serverPort?: number;
@@ -186,9 +205,15 @@ export default class BXO {
186
205
  return this;
187
206
  }
188
207
 
189
- // Plugin system - now accepts other BXO instances
190
- use(bxoInstance: BXO): this {
191
- this.plugins.push(bxoInstance);
208
+ // Plugin system - now accepts both BXO instances and middleware plugins
209
+ use(plugin: BXO | Plugin): this {
210
+ if ('_routes' in plugin) {
211
+ // It's a BXO instance
212
+ this.plugins.push(plugin);
213
+ } else {
214
+ // It's a middleware plugin
215
+ this.middleware.push(plugin);
216
+ }
192
217
  return this;
193
218
  }
194
219
 
@@ -835,6 +860,13 @@ export default class BXO {
835
860
  }
836
861
 
837
862
  try {
863
+ // Run middleware onRequest hooks
864
+ for (const plugin of this.middleware) {
865
+ if (plugin.onRequest) {
866
+ await plugin.onRequest(ctx);
867
+ }
868
+ }
869
+
838
870
  // Run global onRequest hook
839
871
  if (this.hooks.onRequest) {
840
872
  await this.hooks.onRequest(ctx, this);
@@ -850,6 +882,13 @@ export default class BXO {
850
882
  // Execute route handler
851
883
  let response = await route.handler(ctx);
852
884
 
885
+ // Run middleware onResponse hooks
886
+ for (const plugin of this.middleware) {
887
+ if (plugin.onResponse) {
888
+ response = await plugin.onResponse(ctx, response) || response;
889
+ }
890
+ }
891
+
853
892
  // Run global onResponse hook
854
893
  if (this.hooks.onResponse) {
855
894
  response = await this.hooks.onResponse(ctx, response, this) || response;
@@ -1016,6 +1055,13 @@ export default class BXO {
1016
1055
  // Run error hooks
1017
1056
  let errorResponse: any;
1018
1057
 
1058
+ // Run middleware onError hooks
1059
+ for (const plugin of this.middleware) {
1060
+ if (plugin.onError) {
1061
+ errorResponse = await plugin.onError(ctx, error as Error) || errorResponse;
1062
+ }
1063
+ }
1064
+
1019
1065
  if (this.hooks.onError) {
1020
1066
  errorResponse = await this.hooks.onError(ctx, error as Error, this);
1021
1067
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.38",
4
+ "version": "0.0.5-dev.39",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {
package/plugins/cors.ts CHANGED
@@ -8,7 +8,7 @@ interface CORSOptions {
8
8
  maxAge?: number;
9
9
  }
10
10
 
11
- export function cors(options: CORSOptions = {}): BXO {
11
+ export function cors(options: CORSOptions = {}): any {
12
12
  const {
13
13
  origin = '*',
14
14
  methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
@@ -17,14 +17,46 @@ export function cors(options: CORSOptions = {}): BXO {
17
17
  maxAge = 86400
18
18
  } = options;
19
19
 
20
- const corsInstance = new BXO();
20
+ return {
21
+ name: 'cors',
22
+ onRequest: async (ctx: any) => {
23
+ // Handle preflight OPTIONS request
24
+ if (ctx.request.method === 'OPTIONS') {
25
+ const headers: Record<string, string> = {};
21
26
 
22
- corsInstance.onRequest(async (ctx: any) => {
23
- // Handle preflight OPTIONS request
24
- if (ctx.request.method === 'OPTIONS') {
27
+ // Handle origin
28
+ if (typeof origin === 'boolean') {
29
+ if (origin) {
30
+ headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
31
+ }
32
+ } else if (typeof origin === 'string') {
33
+ headers['Access-Control-Allow-Origin'] = origin;
34
+ } else if (Array.isArray(origin)) {
35
+ const requestOrigin = ctx.request.headers.get('origin');
36
+ if (requestOrigin && origin.includes(requestOrigin)) {
37
+ headers['Access-Control-Allow-Origin'] = requestOrigin;
38
+ }
39
+ }
40
+
41
+ headers['Access-Control-Allow-Methods'] = methods.join(', ');
42
+ headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
43
+
44
+ if (credentials) {
45
+ headers['Access-Control-Allow-Credentials'] = 'true';
46
+ }
47
+
48
+ headers['Access-Control-Max-Age'] = maxAge.toString();
49
+
50
+ ctx.set.status = 204;
51
+ ctx.set.headers = { ...ctx.set.headers, ...headers };
52
+
53
+ throw new Response(null, { status: 204, headers });
54
+ }
55
+ },
56
+ onResponse: async (ctx: any, response: any) => {
25
57
  const headers: Record<string, string> = {};
26
58
 
27
- // Handle origin
59
+ // Handle origin for actual requests
28
60
  if (typeof origin === 'boolean') {
29
61
  if (origin) {
30
62
  headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
@@ -38,46 +70,26 @@ export function cors(options: CORSOptions = {}): BXO {
38
70
  }
39
71
  }
40
72
 
41
- headers['Access-Control-Allow-Methods'] = methods.join(', ');
42
- headers['Access-Control-Allow-Headers'] = allowedHeaders.join(', ');
43
-
44
73
  if (credentials) {
45
74
  headers['Access-Control-Allow-Credentials'] = 'true';
46
75
  }
47
-
48
- headers['Access-Control-Max-Age'] = maxAge.toString();
49
-
50
- ctx.set.status = 204;
51
- ctx.set.headers = { ...ctx.set.headers, ...headers };
52
-
53
- throw new Response(null, { status: 204, headers });
54
- }
55
- });
56
-
57
- corsInstance.onResponse(async (ctx: any, response: any) => {
58
- const headers: Record<string, string> = {};
59
76
 
60
- // Handle origin for actual requests
61
- if (typeof origin === 'boolean') {
62
- if (origin) {
63
- headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
77
+ // If response is a Response object, add headers to it
78
+ if (response instanceof Response) {
79
+ const newHeaders = new Headers(response.headers);
80
+ Object.entries(headers).forEach(([key, value]) => {
81
+ newHeaders.set(key, value);
82
+ });
83
+ return new Response(response.body, {
84
+ status: response.status,
85
+ statusText: response.statusText,
86
+ headers: newHeaders
87
+ });
64
88
  }
65
- } else if (typeof origin === 'string') {
66
- headers['Access-Control-Allow-Origin'] = origin;
67
- } else if (Array.isArray(origin)) {
68
- const requestOrigin = ctx.request.headers.get('origin');
69
- if (requestOrigin && origin.includes(requestOrigin)) {
70
- headers['Access-Control-Allow-Origin'] = requestOrigin;
71
- }
72
- }
73
89
 
74
- if (credentials) {
75
- headers['Access-Control-Allow-Credentials'] = 'true';
90
+ // Otherwise, set headers in context for the framework to handle
91
+ ctx.set.headers = { ...ctx.set.headers, ...headers };
92
+ return response;
76
93
  }
77
-
78
- ctx.set.headers = { ...ctx.set.headers, ...headers };
79
- return response;
80
- });
81
-
82
- return corsInstance;
94
+ };
83
95
  }
@@ -52,7 +52,7 @@ class RateLimitStore {
52
52
  }
53
53
  }
54
54
 
55
- export function rateLimit(options: RateLimitOptions): BXO {
55
+ export function rateLimit(options: RateLimitOptions): any {
56
56
  const {
57
57
  max,
58
58
  window,
@@ -74,67 +74,63 @@ export function rateLimit(options: RateLimitOptions): BXO {
74
74
  // Cleanup expired entries every 5 minutes
75
75
  setInterval(() => store.cleanup(), 5 * 60 * 1000);
76
76
 
77
- const rateLimitInstance = new BXO();
77
+ return {
78
+ name: 'rateLimit',
79
+ onRequest: async (ctx: any) => {
80
+ const url = new URL(ctx.request.url);
81
+ const pathname = url.pathname;
82
+
83
+ // Skip rate limiting for excluded paths
84
+ if (exclude.some(path => {
85
+ if (path.includes('*')) {
86
+ const regex = new RegExp(path.replace(/\*/g, '.*'));
87
+ return regex.test(pathname);
88
+ }
89
+ return pathname === path || pathname.startsWith(path);
90
+ })) {
91
+ return;
92
+ }
78
93
 
79
- rateLimitInstance.onRequest(async (ctx: any) => {
80
- const url = new URL(ctx.request.url);
81
- const pathname = url.pathname;
82
-
83
- // Skip rate limiting for excluded paths
84
- if (exclude.some(path => {
85
- if (path.includes('*')) {
86
- const regex = new RegExp(path.replace(/\*/g, '.*'));
87
- return regex.test(pathname);
94
+ const key = keyGenerator(ctx);
95
+ const entry = store.increment(key, window);
96
+
97
+ if (entry.count > max) {
98
+ const resetTime = Math.ceil(entry.resetTime / 1000);
99
+ throw new Response(JSON.stringify({
100
+ error: message,
101
+ retryAfter: resetTime
102
+ }), {
103
+ status: statusCode,
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ 'X-RateLimit-Limit': max.toString(),
107
+ 'X-RateLimit-Remaining': '0',
108
+ 'X-RateLimit-Reset': resetTime.toString(),
109
+ 'Retry-After': resetTime.toString()
110
+ }
111
+ });
88
112
  }
89
- return pathname === path || pathname.startsWith(path);
90
- })) {
91
- return;
92
- }
93
113
 
94
- const key = keyGenerator(ctx);
95
- const entry = store.increment(key, window);
96
-
97
- if (entry.count > max) {
98
- const resetTime = Math.ceil(entry.resetTime / 1000);
99
- throw new Response(JSON.stringify({
100
- error: message,
101
- retryAfter: resetTime - Math.floor(Date.now() / 1000)
102
- }), {
103
- status: statusCode,
104
- headers: {
105
- 'Content-Type': 'application/json',
106
- 'X-RateLimit-Limit': max.toString(),
107
- 'X-RateLimit-Remaining': '0',
108
- 'X-RateLimit-Reset': resetTime.toString(),
109
- 'Retry-After': (resetTime - Math.floor(Date.now() / 1000)).toString()
110
- }
111
- });
112
- }
113
-
114
- // Add rate limit headers
115
- ctx.set.headers = {
116
- ...ctx.set.headers,
117
- 'X-RateLimit-Limit': max.toString(),
118
- 'X-RateLimit-Remaining': Math.max(0, max - entry.count).toString(),
119
- 'X-RateLimit-Reset': Math.ceil(entry.resetTime / 1000).toString()
120
- };
121
- });
114
+ // Add rate limit headers to context
115
+ ctx.set.headers = {
116
+ ...ctx.set.headers,
117
+ 'X-RateLimit-Limit': max.toString(),
118
+ 'X-RateLimit-Remaining': Math.max(0, max - entry.count).toString(),
119
+ 'X-RateLimit-Reset': Math.ceil(entry.resetTime / 1000).toString()
120
+ };
121
+ },
122
+ onResponse: async (ctx: any, response: any) => {
123
+ // Skip successful requests if configured
124
+ if (skipSuccessful && response instanceof Response && response.status < 400) {
125
+ return response;
126
+ }
122
127
 
123
- rateLimitInstance.onResponse(async (ctx: any, response: any) => {
124
- const status = ctx.set.status || 200;
125
- const key = keyGenerator(ctx);
126
-
127
- // Optionally skip counting successful or failed requests
128
- if ((skipSuccessful && status < 400) || (skipFailed && status >= 400)) {
129
- // Decrement the counter since we don't want to count this request
130
- const entry = store.get(key);
131
- if (entry && entry.count > 0) {
132
- store.set(key, entry.count - 1, entry.resetTime);
128
+ // Skip failed requests if configured
129
+ if (skipFailed && response instanceof Response && response.status >= 400) {
130
+ return response;
133
131
  }
134
- }
135
-
136
- return response;
137
- });
138
132
 
139
- return rateLimitInstance;
133
+ return response;
134
+ }
135
+ };
140
136
  }