bxo 0.0.5-dev.65 → 0.0.5-dev.66

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.
Files changed (37) hide show
  1. package/README.md +83 -675
  2. package/example/cors-example.ts +49 -0
  3. package/example/index.html +5 -0
  4. package/example/index.ts +57 -0
  5. package/package.json +9 -15
  6. package/plugins/cors.ts +124 -98
  7. package/plugins/index.ts +2 -9
  8. package/plugins/openapi.ts +130 -0
  9. package/src/index.ts +646 -59
  10. package/tsconfig.json +3 -5
  11. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
  12. package/examples/serve-react/README.md +0 -15
  13. package/examples/serve-react/app.tsx +0 -8
  14. package/examples/serve-react/bun.lock +0 -42
  15. package/examples/serve-react/index.html +0 -9
  16. package/examples/serve-react/index.ts +0 -27
  17. package/examples/serve-react/package.json +0 -17
  18. package/examples/serve-react/tsconfig.json +0 -29
  19. package/index.ts +0 -5
  20. package/plugins/README.md +0 -160
  21. package/plugins/ratelimit.ts +0 -136
  22. package/src/core/bxo.ts +0 -458
  23. package/src/handlers/request-handler.ts +0 -230
  24. package/src/types/index.ts +0 -167
  25. package/src/utils/context-factory.ts +0 -158
  26. package/src/utils/helpers.ts +0 -40
  27. package/src/utils/index.ts +0 -448
  28. package/src/utils/response-handler.ts +0 -293
  29. package/src/utils/route-matcher.ts +0 -191
  30. package/tests/README.md +0 -359
  31. package/tests/integration/bxo.test.ts +0 -616
  32. package/tests/run-tests.ts +0 -44
  33. package/tests/unit/context-factory.test.ts +0 -386
  34. package/tests/unit/helpers.test.ts +0 -253
  35. package/tests/unit/response-handler.test.ts +0 -327
  36. package/tests/unit/route-matcher.test.ts +0 -181
  37. package/tests/unit/utils.test.ts +0 -475
@@ -1,327 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'bun:test';
2
- import { processResponse, createErrorResponse, createValidationErrorResponse } from '../../src/utils/response-handler';
3
- import type { Context, InternalCookie } from '../../src/types';
4
-
5
- describe('Response Handler', () => {
6
- let mockContext: Context;
7
- let mockInternalCookies: InternalCookie[];
8
-
9
- beforeEach(() => {
10
- mockContext = {
11
- params: {},
12
- query: {},
13
- body: {},
14
- headers: {},
15
- cookies: {},
16
- path: '/',
17
- request: new Request('http://localhost:3000/'),
18
- set: {
19
- status: 200,
20
- headers: {},
21
- cookies: () => { }
22
- },
23
- status: () => { },
24
- redirect: () => new Response(),
25
- clearRedirect: () => { }
26
- } as any;
27
-
28
- mockInternalCookies = [
29
- {
30
- name: 'session',
31
- value: 'abc123',
32
- domain: 'example.com',
33
- path: '/',
34
- secure: true,
35
- httpOnly: true
36
- }
37
- ];
38
- });
39
-
40
- describe('processResponse', () => {
41
- it('should handle implicit redirect intent', () => {
42
- mockContext.set.redirect = { location: '/new-location', status: 301 };
43
-
44
- const result = processResponse(undefined, mockContext, mockInternalCookies, true);
45
-
46
- expect(result.status).toBe(301);
47
- expect(result.headers.get('location')).toBe('/new-location');
48
- });
49
-
50
- it('should handle implicit redirect with status from set', () => {
51
- mockContext.set.status = 307;
52
-
53
- const result = processResponse(undefined, mockContext, mockInternalCookies, true);
54
-
55
- expect(result.status).toBe(307);
56
- });
57
-
58
- it('should handle redirect with cookies', () => {
59
- mockContext.set.redirect = { location: '/new-location', status: 302 };
60
-
61
- const result = processResponse(undefined, mockContext, mockInternalCookies, true);
62
-
63
- expect(result.status).toBe(302);
64
- expect(result.headers.get('location')).toBe('/new-location');
65
- expect(result.headers.get('set-cookie')).toContain('session=abc123');
66
- });
67
-
68
- it('should validate response with schema when enabled', () => {
69
- const routeConfig = {
70
- response: {
71
- 200: { parse: (data: any) => data }
72
- }
73
- };
74
-
75
- const result = processResponse({ message: 'success' }, mockContext, mockInternalCookies, true, routeConfig);
76
-
77
- expect(result.status).toBe(200);
78
- expect(result.headers.get('content-type')).toBe('application/json');
79
- });
80
-
81
- it('should handle validation errors', () => {
82
- const routeConfig = {
83
- response: {
84
- 200: { parse: () => { throw new Error('Validation failed'); } }
85
- }
86
- };
87
-
88
- const result = processResponse({ message: 'success' }, mockContext, mockInternalCookies, true, routeConfig);
89
-
90
- expect(result.status).toBe(500);
91
- expect(result.headers.get('content-type')).toBe('application/json');
92
- });
93
-
94
- it('should handle Response objects', () => {
95
- const response = new Response('Hello World', { status: 200 });
96
- mockContext.set.headers = { 'x-custom': 'value' };
97
-
98
- const result = processResponse(response, mockContext, mockInternalCookies, true);
99
-
100
- expect(result.status).toBe(200);
101
- expect(result.headers.get('x-custom')).toBe('value');
102
- expect(result.headers.get('set-cookie')).toContain('session=abc123');
103
- });
104
-
105
- it('should handle File objects', () => {
106
- const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
107
-
108
- const result = processResponse(file, mockContext, mockInternalCookies, true);
109
-
110
- expect(result.status).toBe(200);
111
- expect(result.headers.get('content-type')).toBe('text/plain;charset=utf-8');
112
- expect(result.headers.get('content-length')).toBe('12');
113
- });
114
-
115
- it('should handle Bun.file objects', () => {
116
- const bunFile = Bun.file('test.txt');
117
-
118
- const result = processResponse(bunFile, mockContext, mockInternalCookies, true);
119
-
120
- expect(result.status).toBe(200);
121
- expect(result.headers.get('content-type')).toBe('text/plain;charset=utf-8');
122
- });
123
-
124
- it('should handle string responses', async () => {
125
- const result = processResponse('Hello World', mockContext, mockInternalCookies, true);
126
-
127
- expect(result.status).toBe(200);
128
- expect(result.headers.get('content-type')).toBe('text/plain');
129
- expect(await result.text()).toBe('Hello World');
130
- });
131
-
132
- it('should preserve custom Content-Type for string responses', async () => {
133
- mockContext.set.headers = { 'Content-Type': 'text/html' };
134
- const result = processResponse('<html><body>Hello World</body></html>', mockContext, mockInternalCookies, true);
135
-
136
- expect(result.status).toBe(200);
137
- expect(result.headers.get('content-type')).toBe('text/html');
138
- expect(await result.text()).toBe('<html><body>Hello World</body></html>');
139
- });
140
-
141
- it('should preserve custom Content-Type for string responses without cookies', async () => {
142
- mockContext.set.headers = { 'Content-Type': 'text/html' };
143
- const result = processResponse('<html><body>Hello World</body></html>', mockContext, [], true);
144
-
145
- expect(result.status).toBe(200);
146
- expect(result.headers.get('content-type')).toBe('text/html');
147
- expect(await result.text()).toBe('<html><body>Hello World</body></html>');
148
- });
149
-
150
- it('should default to text/plain when no Content-Type is set for string responses', async () => {
151
- const result = processResponse('Hello World', mockContext, [], true);
152
-
153
- expect(result.status).toBe(200);
154
- expect(result.headers.get('content-type')).toBe('text/plain');
155
- expect(await result.text()).toBe('Hello World');
156
- });
157
-
158
- it('should handle object responses', async () => {
159
- const result = processResponse({ message: 'success' }, mockContext, mockInternalCookies, true);
160
-
161
- expect(result.status).toBe(200);
162
- expect(result.headers.get('content-type')).toBe('application/json');
163
- expect(await result.json()).toEqual({ message: 'success' });
164
- });
165
-
166
- it('should handle responses with cookies', async () => {
167
- const result = processResponse({ message: 'success' }, mockContext, mockInternalCookies, true);
168
-
169
- const setCookieHeader = result.headers.get('set-cookie');
170
- expect(setCookieHeader).toMatch(/session=abc123/);
171
- });
172
-
173
- it('should handle responses without cookies', () => {
174
- const result = processResponse({ message: 'success' }, mockContext, [], true);
175
-
176
- expect(result.headers.get('set-cookie')).toBeNull();
177
- });
178
-
179
- it('should use status from context', () => {
180
- mockContext.set.status = 201;
181
-
182
- const result = processResponse({ message: 'created' }, mockContext, [], true);
183
-
184
- expect(result.status).toBe(201);
185
- });
186
-
187
- it('should handle custom headers from context', () => {
188
- mockContext.set.headers = {
189
- 'x-custom': 'value',
190
- 'authorization': 'Bearer token'
191
- };
192
-
193
- const result = processResponse({ message: 'success' }, mockContext, [], true);
194
-
195
- expect(result.headers.get('x-custom')).toBe('value');
196
- expect(result.headers.get('authorization')).toBe('Bearer token');
197
- });
198
- });
199
-
200
- describe('createErrorResponse', () => {
201
- it('should create error response with Error object', async () => {
202
- const error = new Error('Something went wrong');
203
- const result = createErrorResponse(error, 500);
204
-
205
- expect(result.status).toBe(500);
206
- expect(result.headers.get('content-type')).toBe('application/json');
207
-
208
- const body = await result.json() as { error: string };
209
- expect(body.error).toBe('Something went wrong');
210
- });
211
-
212
- it('should create error response with string', async () => {
213
- const result = createErrorResponse('Custom error message', 400);
214
-
215
- expect(result.status).toBe(400);
216
-
217
- const body = await result.json() as { error: string };
218
- expect(body.error).toBe('Custom error message');
219
- });
220
-
221
- it('should create error response with details', async () => {
222
- const error = new Error('Validation failed');
223
- const details = { field: 'email', message: 'Invalid format' };
224
- const result = createErrorResponse(error, 422, details);
225
-
226
- expect(result.status).toBe(422);
227
-
228
- const body = await result.json() as { error: string, details: any };
229
- expect(body.error).toBe('Validation failed');
230
- expect(body.details).toEqual(details);
231
- });
232
-
233
- it('should use default status when not provided', () => {
234
- const result = createErrorResponse('Error');
235
-
236
- expect(result.status).toBe(500);
237
- });
238
- });
239
-
240
- describe('createValidationErrorResponse', () => {
241
- it('should create validation error response with Zod errors', async () => {
242
- const validationError = {
243
- message: 'Validation failed',
244
- errors: [
245
- { field: 'email', message: 'Invalid email' },
246
- { field: 'name', message: 'Name is required' }
247
- ]
248
- };
249
-
250
- const result = createValidationErrorResponse(validationError, 400);
251
-
252
- expect(result.status).toBe(400);
253
-
254
- const body = await result.json() as { error: string, details: any };
255
- expect(body.error).toBe('Validation failed for 2 field(s)');
256
- expect(body.details).toEqual(validationError.errors);
257
- });
258
-
259
- it('should create validation error response with Zod issues', async () => {
260
- const validationError = {
261
- message: 'Validation failed',
262
- issues: [
263
- { path: ['email'], message: 'Invalid email' },
264
- { path: ['name'], message: 'Name is required' }
265
- ]
266
- };
267
-
268
- const result = createValidationErrorResponse(validationError, 400);
269
-
270
- expect(result.status).toBe(400);
271
-
272
- const body = await result.json() as { error: string, details: any };
273
- expect(body.error).toBe('Validation failed for 2 field(s)');
274
- expect(body.details).toEqual(validationError.issues);
275
- });
276
-
277
- it('should handle validation error without details', async () => {
278
- const validationError = new Error('Validation failed');
279
-
280
- const result = createValidationErrorResponse(validationError, 400);
281
-
282
- expect(result.status).toBe(400);
283
-
284
- const body = await result.json() as { error: string, details: any };
285
- expect(body.error).toBe('Validation failed');
286
- expect(body.details).toBeUndefined();
287
- });
288
-
289
- it('should use default status when not provided', () => {
290
- const validationError = new Error('Validation failed');
291
- const result = createValidationErrorResponse(validationError);
292
-
293
- expect(result.status).toBe(400);
294
- });
295
- });
296
-
297
- describe('Edge Cases', () => {
298
- it('should handle null response', () => {
299
- const result = processResponse(null, mockContext, [], true);
300
-
301
- expect(result.status).toBe(200);
302
- expect(result.headers.get('content-type')).toBe('application/json');
303
- });
304
-
305
- it('should handle undefined response', () => {
306
- const result = processResponse(undefined, mockContext, [], true);
307
-
308
- expect(result.status).toBe(200);
309
- expect(result.headers.get('content-type')).toBe('application/json');
310
- });
311
-
312
- it('should handle empty object response', async () => {
313
- const result = processResponse({}, mockContext, [], true);
314
-
315
- expect(result.status).toBe(200);
316
- expect(result.headers.get('content-type')).toBe('application/json');
317
- expect(await result.json()).toEqual({});
318
- });
319
-
320
- it('should handle response with null values', async () => {
321
- const result = processResponse({ data: null, message: 'success' }, mockContext, [], true);
322
-
323
- expect(result.status).toBe(200);
324
- expect(await result.json()).toEqual({ data: null, message: 'success' });
325
- });
326
- });
327
- });
@@ -1,181 +0,0 @@
1
- import { describe, it, expect } from 'bun:test';
2
- import { matchRoute, matchWSRoute } from '../../src/utils/route-matcher';
3
- import type { Route, WSRoute } from '../../src/types';
4
-
5
- describe('Route Matcher', () => {
6
- describe('HTTP Route Matching', () => {
7
- const routes: Route[] = [
8
- { method: 'GET', path: '/', handler: () => 'home' },
9
- { method: 'GET', path: '/users', handler: () => 'users' },
10
- { method: 'GET', path: '/users/:id', handler: () => 'user' },
11
- { method: 'POST', path: '/users', handler: () => 'create' },
12
- { method: 'GET', path: '/users/:id/posts', handler: () => 'posts' },
13
- { method: 'GET', path: '/users/:id/posts/:postId', handler: () => 'post' },
14
- { method: 'GET', path: '/files/*', handler: () => 'files' },
15
- { method: 'GET', path: '/docs/**', handler: () => 'docs' },
16
- { method: 'GET', path: '/api/v1/users', handler: () => 'api' }
17
- ];
18
-
19
- it('should match exact routes', () => {
20
- const result = matchRoute('GET', '/', routes);
21
- expect(result).toBeDefined();
22
- expect(result?.route.path).toBe('/');
23
- expect(result?.params).toEqual({});
24
- });
25
-
26
- it('should match routes with parameters', () => {
27
- const result = matchRoute('GET', '/users/123', routes);
28
- expect(result).toBeDefined();
29
- expect(result?.route.path).toBe('/users/:id');
30
- expect(result?.params).toEqual({ id: '123' });
31
- });
32
-
33
- it('should match nested routes with parameters', () => {
34
- const result = matchRoute('GET', '/users/123/posts/456', routes);
35
- expect(result).toBeDefined();
36
- expect(result?.route.path).toBe('/users/:id/posts/:postId');
37
- expect(result?.params).toEqual({ id: '123', postId: '456' });
38
- });
39
-
40
- it('should match wildcard routes', () => {
41
- const result = matchRoute('GET', '/files/image.jpg', routes);
42
- expect(result).toBeDefined();
43
- expect(result?.route.path).toBe('/files/*');
44
- expect(result?.params).toEqual({ '*0': 'image.jpg' });
45
- });
46
-
47
- it('should match double wildcard routes', () => {
48
- const result = matchRoute('GET', '/docs/api/reference', routes);
49
- expect(result).toBeDefined();
50
- expect(result?.route.path).toBe('/docs/**');
51
- expect(result?.params).toEqual({ '**0': 'api/reference' });
52
- });
53
-
54
- it('should not match wrong method', () => {
55
- const result = matchRoute('PUT', '/users', routes);
56
- expect(result).toBeNull();
57
- });
58
-
59
- it('should not match non-existent route', () => {
60
- const result = matchRoute('GET', '/nonexistent', routes);
61
- expect(result).toBeNull();
62
- });
63
-
64
- it('should handle complex wildcard scenarios', () => {
65
- const complexRoutes: Route[] = [
66
- { method: 'GET', path: '/api/**/users/:id', handler: () => 'complex' },
67
- { method: 'GET', path: '/api/**/posts/*', handler: () => 'posts' }
68
- ];
69
-
70
- const result1 = matchRoute('GET', '/api/v1/admin/users/123', complexRoutes);
71
- expect(result1).toBeDefined();
72
- expect(result1?.route.path).toBe('/api/**/users/:id');
73
- // The double wildcard captures everything up to the next segment
74
- expect(result1?.params).toEqual({ '**0': 'v1/admin', id: '123' });
75
-
76
- const result2 = matchRoute('GET', '/api/v1/admin/posts/456', complexRoutes);
77
- expect(result2).toBeDefined();
78
- expect(result2?.route.path).toBe('/api/**/posts/*');
79
- expect(result2?.params).toEqual({ '**0': 'v1/admin', '*1': '456' });
80
- });
81
-
82
- it('should handle multiple double wildcards', () => {
83
- const multiWildcardRoutes: Route[] = [
84
- { method: 'GET', path: '/api/**/users/**/posts/:id', handler: () => 'multi' }
85
- ];
86
-
87
- const result = matchRoute('GET', '/api/v1/admin/users/123/active/posts/456', multiWildcardRoutes);
88
- expect(result).toBeDefined();
89
- expect(result?.route.path).toBe('/api/**/users/**/posts/:id');
90
- // Multiple double wildcards get unique keys
91
- expect(result?.params).toEqual({ '**0': 'v1/admin', '**1': '123/active', id: '456' });
92
- });
93
-
94
- it('should handle mixed wildcard patterns', () => {
95
- const mixedRoutes: Route[] = [
96
- { method: 'GET', path: '/api/*/users/**/posts/*', handler: () => 'mixed' }
97
- ];
98
-
99
- const result = matchRoute('GET', '/api/v1/users/123/active/posts/456', mixedRoutes);
100
- expect(result).toBeDefined();
101
- expect(result?.route.path).toBe('/api/*/users/**/posts/*');
102
- // Mixed wildcards get unique keys
103
- expect(result?.params).toEqual({ '*0': 'v1', '**1': '123/active', '*2': '456' });
104
- });
105
- });
106
-
107
- describe('WebSocket Route Matching', () => {
108
- const wsRoutes: WSRoute[] = [
109
- { path: '/ws', handler: {} },
110
- { path: '/ws/chat', handler: {} },
111
- { path: '/ws/chat/:roomId', handler: {} },
112
- { path: '/ws/files/*', handler: {} },
113
- { path: '/ws/docs/**', handler: {} }
114
- ];
115
-
116
- it('should match exact WebSocket routes', () => {
117
- const result = matchWSRoute('/ws', wsRoutes);
118
- expect(result).toBeDefined();
119
- expect(result?.route.path).toBe('/ws');
120
- expect(result?.params).toEqual({});
121
- });
122
-
123
- it('should match WebSocket routes with parameters', () => {
124
- const result = matchWSRoute('/ws/chat/room123', wsRoutes);
125
- expect(result).toBeDefined();
126
- expect(result?.route.path).toBe('/ws/chat/:roomId');
127
- expect(result?.params).toEqual({ roomId: 'room123' });
128
- });
129
-
130
- it('should match WebSocket wildcard routes', () => {
131
- const result = matchWSRoute('/ws/files/document.pdf', wsRoutes);
132
- expect(result).toBeDefined();
133
- expect(result?.route.path).toBe('/ws/files/*');
134
- expect(result?.params).toEqual({ '*0': 'document.pdf' });
135
- });
136
-
137
- it('should match WebSocket double wildcard routes', () => {
138
- const result = matchWSRoute('/ws/docs/api/v1', wsRoutes);
139
- expect(result).toBeDefined();
140
- expect(result?.route.path).toBe('/ws/docs/**');
141
- expect(result?.params).toEqual({ '**0': 'api/v1' });
142
- });
143
-
144
- it('should not match non-existent WebSocket route', () => {
145
- const result = matchWSRoute('/ws/nonexistent', wsRoutes);
146
- expect(result).toBeNull();
147
- });
148
- });
149
-
150
- describe('Edge Cases', () => {
151
- it('should handle empty routes array', () => {
152
- const result = matchRoute('GET', '/', []);
153
- expect(result).toBeNull();
154
- });
155
-
156
- it('should handle routes with empty path', () => {
157
- const routes: Route[] = [
158
- { method: 'GET', path: '', handler: () => 'empty' }
159
- ];
160
- const result = matchRoute('GET', '', routes);
161
- expect(result).toBeDefined();
162
- });
163
-
164
- it('should handle routes with trailing slashes', () => {
165
- const routes: Route[] = [
166
- { method: 'GET', path: '/users/', handler: () => 'users' }
167
- ];
168
- const result = matchRoute('GET', '/users/', routes);
169
- expect(result).toBeDefined();
170
- });
171
-
172
- it('should handle multiple wildcards in same route', () => {
173
- const routes: Route[] = [
174
- { method: 'GET', path: '/api/*/users/*', handler: () => 'multi' }
175
- ];
176
- const result = matchRoute('GET', '/api/v1/users/123', routes);
177
- expect(result).toBeDefined();
178
- expect(result?.params).toEqual({ '*0': 'v1', '*1': '123' }); // Each wildcard gets a unique key
179
- });
180
- });
181
- });