bxo 0.0.5-dev.20 โ†’ 0.0.5-dev.22

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
@@ -20,7 +20,7 @@ BXO is a fast, lightweight, and fully type-safe web framework built specifically
20
20
  ### Installation
21
21
 
22
22
  ```bash
23
- bun add zod
23
+ bun add bxo
24
24
  ```
25
25
 
26
26
  ### Basic Usage
package/index.ts CHANGED
@@ -3,6 +3,31 @@ import { z } from 'zod';
3
3
  // Type utilities for extracting types from Zod schemas
4
4
  type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
5
5
 
6
+ // Response configuration types
7
+ type ResponseSchema = z.ZodSchema<any>;
8
+ type StatusResponseSchema = Record<number, ResponseSchema>;
9
+ type ResponseConfig = ResponseSchema | StatusResponseSchema;
10
+
11
+ // Type utility to extract response type from response config
12
+ type InferResponseType<T> = T extends ResponseSchema
13
+ ? InferZodType<T>
14
+ : T extends StatusResponseSchema
15
+ ? { [K in keyof T]: InferZodType<T[K]> }[keyof T]
16
+ : never;
17
+
18
+ // Cookie interface
19
+ interface Cookie {
20
+ name: string;
21
+ value: string;
22
+ domain?: string;
23
+ path?: string;
24
+ expires?: Date;
25
+ maxAge?: number;
26
+ secure?: boolean;
27
+ httpOnly?: boolean;
28
+ sameSite?: 'Strict' | 'Lax' | 'None';
29
+ }
30
+
6
31
  // OpenAPI detail information
7
32
  interface RouteDetail {
8
33
  summary?: string;
@@ -21,7 +46,8 @@ interface RouteConfig {
21
46
  query?: z.ZodSchema<any>;
22
47
  body?: z.ZodSchema<any>;
23
48
  headers?: z.ZodSchema<any>;
24
- response?: z.ZodSchema<any>;
49
+ cookies?: z.ZodSchema<any>;
50
+ response?: ResponseConfig;
25
51
  detail?: RouteDetail;
26
52
  }
27
53
 
@@ -31,17 +57,20 @@ export type Context<TConfig extends RouteConfig = {}> = {
31
57
  query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
32
58
  body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
33
59
  headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
60
+ cookies: TConfig['cookies'] extends z.ZodSchema<any> ? InferZodType<TConfig['cookies']> : Record<string, string>;
34
61
  path: string;
35
62
  request: Request;
36
63
  set: {
37
64
  status?: number;
38
65
  headers?: Record<string, string>;
66
+ cookies?: Cookie[];
39
67
  };
68
+ status: (code: number, data?: any) => any;
40
69
  [key: string]: any;
41
70
  };
42
71
 
43
- // Handler function type
44
- type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<any> | any;
72
+ // Handler function type with proper response typing
73
+ type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
45
74
 
46
75
  // Route definition
47
76
  interface Route {
@@ -394,12 +423,61 @@ export default class BXO {
394
423
  return headerObj;
395
424
  }
396
425
 
426
+ // Parse cookies from Cookie header
427
+ private parseCookies(cookieHeader: string | null): Record<string, string> {
428
+ const cookies: Record<string, string> = {};
429
+
430
+ if (!cookieHeader) return cookies;
431
+
432
+ const cookiePairs = cookieHeader.split(';');
433
+ for (const pair of cookiePairs) {
434
+ const [name, value] = pair.trim().split('=');
435
+ if (name && value) {
436
+ cookies[decodeURIComponent(name)] = decodeURIComponent(value);
437
+ }
438
+ }
439
+
440
+ return cookies;
441
+ }
442
+
397
443
  // Validate data against Zod schema
398
444
  private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
399
445
  if (!schema) return data;
400
446
  return schema.parse(data);
401
447
  }
402
448
 
449
+ // Validate response against response config (supports both simple and status-based schemas)
450
+ private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
451
+ if (!responseConfig) return data;
452
+
453
+ // If it's a simple schema (not status-based)
454
+ if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
455
+ return responseConfig.parse(data);
456
+ }
457
+
458
+ // If it's a status-based schema
459
+ if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
460
+ const statusSchema = responseConfig[status];
461
+ if (statusSchema) {
462
+ return statusSchema.parse(data);
463
+ }
464
+
465
+ // If no specific status schema found, try to find a fallback
466
+ // Common fallback statuses: 200, 201, 400, 500
467
+ const fallbackStatuses = [200, 201, 400, 500];
468
+ for (const fallbackStatus of fallbackStatuses) {
469
+ if (responseConfig[fallbackStatus]) {
470
+ return responseConfig[fallbackStatus].parse(data);
471
+ }
472
+ }
473
+
474
+ // If no schema found for the status, return data as-is
475
+ return data;
476
+ }
477
+
478
+ return data;
479
+ }
480
+
403
481
  // Main request handler
404
482
  private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
405
483
  const url = new URL(request.url);
@@ -433,6 +511,7 @@ export default class BXO {
433
511
  const { route, params } = matchResult;
434
512
  const query = this.parseQuery(url.searchParams);
435
513
  const headers = this.parseHeaders(request.headers);
514
+ const cookies = this.parseCookies(request.headers.get('cookie'));
436
515
 
437
516
  let body: any;
438
517
  if (request.method !== 'GET' && request.method !== 'HEAD') {
@@ -470,15 +549,21 @@ export default class BXO {
470
549
  const validatedQuery = route.config?.query ? this.validateData(route.config.query, query) : query;
471
550
  const validatedBody = route.config?.body ? this.validateData(route.config.body, body) : body;
472
551
  const validatedHeaders = route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
552
+ const validatedCookies = route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
473
553
 
474
554
  ctx = {
475
555
  params: validatedParams,
476
556
  query: validatedQuery,
477
557
  body: validatedBody,
478
558
  headers: validatedHeaders,
559
+ cookies: validatedCookies,
479
560
  path: pathname,
480
561
  request,
481
- set: {}
562
+ set: {},
563
+ status: (code: number, data?: any) => {
564
+ ctx.set.status = code;
565
+ return data;
566
+ }
482
567
  };
483
568
  } catch (validationError) {
484
569
  // Validation failed - return error response
@@ -539,7 +624,8 @@ export default class BXO {
539
624
  if (route.config?.response && !(response instanceof Response)) {
540
625
  try {
541
626
  console.log('response', response);
542
- response = this.validateData(route.config.response, response);
627
+ const status = ctx.set.status || 200;
628
+ response = this.validateResponse(route.config.response, response, status);
543
629
  } catch (validationError) {
544
630
  // Response validation failed
545
631
 
@@ -602,9 +688,34 @@ export default class BXO {
602
688
  return new Response(bunFile, responseInit);
603
689
  }
604
690
 
691
+ // Prepare headers with cookies
692
+ let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
693
+
694
+ // Handle cookies if any are set
695
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
696
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
697
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
698
+
699
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
700
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
701
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
702
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
703
+ if (cookie.secure) cookieString += `; Secure`;
704
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
705
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
706
+
707
+ return cookieString;
708
+ });
709
+
710
+ // Add Set-Cookie headers
711
+ cookieHeaders.forEach((cookieHeader, index) => {
712
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
713
+ });
714
+ }
715
+
605
716
  const responseInit: ResponseInit = {
606
717
  status: ctx.set.status || 200,
607
- headers: ctx.set.headers || {}
718
+ headers: responseHeaders
608
719
  };
609
720
 
610
721
  if (typeof response === 'string') {
@@ -905,4 +1016,15 @@ const file = (path: string, options?: { type?: string; headers?: Record<string,
905
1016
  export { z, error, file };
906
1017
 
907
1018
  // Export types for external use
908
- export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
1019
+ export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie };
1020
+
1021
+ // Helper function to create a cookie
1022
+ export const createCookie = (
1023
+ name: string,
1024
+ value: string,
1025
+ options: Omit<Cookie, 'name' | 'value'> = {}
1026
+ ): Cookie => ({
1027
+ name,
1028
+ value,
1029
+ ...options
1030
+ });
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.20",
4
+ "version": "0.0.5-dev.22",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {
package/plugins/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  // Export all plugins
2
2
  export { cors } from './cors';
3
- export { logger } from './logger';
4
- export { auth, createJWT } from './auth';
5
3
  export { rateLimit } from './ratelimit';
6
4
 
7
5
  // Import BXO for plugin typing
package/example.ts DELETED
@@ -1,204 +0,0 @@
1
- import BXO, { z } from './index';
2
- import { cors, logger, auth, rateLimit, createJWT } from './plugins';
3
-
4
- // Create a simple API plugin that defines its own routes
5
- function createApiPlugin(): BXO {
6
- const apiPlugin = new BXO();
7
-
8
- apiPlugin
9
- .get('/api/info', async (ctx) => {
10
- return {
11
- name: 'BXO API Plugin',
12
- version: '1.0.0',
13
- endpoints: ['/api/info', '/api/ping', '/api/time']
14
- };
15
- })
16
- .get('/api/ping', async (ctx) => {
17
- return { ping: 'pong', timestamp: Date.now() };
18
- })
19
- .get('/api/time', async (ctx) => {
20
- return { time: new Date().toISOString() };
21
- })
22
- .post('/api/echo', async (ctx) => {
23
- return { echo: ctx.body };
24
- }, {
25
- body: z.object({
26
- message: z.string()
27
- })
28
- });
29
-
30
- return apiPlugin;
31
- }
32
-
33
- // Create the app instance
34
- const app = new BXO();
35
-
36
- // Add plugins (including our new API plugin)
37
- app
38
- .use(logger({ format: 'simple' }))
39
- .use(cors({
40
- origin: ['http://localhost:3000', 'https://example.com'],
41
- credentials: true
42
- }))
43
- .use(rateLimit({
44
- max: 100,
45
- window: 60, // 1 minute
46
- exclude: ['/health']
47
- }))
48
- .use(auth({
49
- type: 'jwt',
50
- secret: 'your-secret-key',
51
- exclude: ['/', '/login', '/health', '/api/*']
52
- }))
53
- .use(createApiPlugin()); // Add our plugin with actual routes
54
-
55
- // Add simplified lifecycle hooks
56
- app
57
- .onBeforeStart(() => {
58
- console.log('๐Ÿ”ง Preparing to start server...');
59
- })
60
- .onAfterStart(() => {
61
- console.log('โœ… Server fully started and ready!');
62
- })
63
- .onBeforeStop(() => {
64
- console.log('๐Ÿ”ง Preparing to stop server...');
65
- })
66
- .onAfterStop(() => {
67
- console.log('โœ… Server fully stopped!');
68
- })
69
- .onRequest((ctx) => {
70
- console.log(`๐Ÿ“จ Processing ${ctx.request.method} ${ctx.request.url}`);
71
- })
72
- .onResponse((ctx, response) => {
73
- console.log(`๐Ÿ“ค Response sent for ${ctx.request.method} ${ctx.request.url}`);
74
- return response;
75
- })
76
- .onError((ctx, error) => {
77
- console.error(`๐Ÿ’ฅ Error in ${ctx.request.method} ${ctx.request.url}:`, error.message);
78
- return { error: 'Something went wrong', timestamp: new Date().toISOString() };
79
- });
80
-
81
- // Routes exactly like your example
82
- app
83
- // Two arguments: path, handler
84
- .get('/simple', async (ctx) => {
85
- return { message: 'Hello World' };
86
- })
87
-
88
- // Three arguments: path, handler, config
89
- .get('/users/:id', async (ctx) => {
90
- // ctx.params.id is fully typed as string (UUID)
91
- // ctx.query.include is typed as string | undefined
92
- return { user: { id: ctx.params.id, include: ctx.query.include } };
93
- }, {
94
- params: z.object({ id: z.string().uuid() }),
95
- query: z.object({ include: z.string().optional() })
96
- })
97
-
98
- .post('/users', async (ctx) => {
99
- // ctx.body is fully typed with name: string, email: string
100
- return { created: ctx.body };
101
- }, {
102
- body: z.object({
103
- name: z.string(),
104
- email: z.string().email()
105
- })
106
- })
107
-
108
- // Additional examples
109
- .get('/health', async (ctx) => {
110
- return {
111
- status: 'ok',
112
- timestamp: new Date().toISOString(),
113
- server: app.getServerInfo()
114
- };
115
- })
116
-
117
- .post('/login', async (ctx) => {
118
- const { username, password } = ctx.body;
119
-
120
- // Simple auth check (in production, verify against database)
121
- if (username === 'admin' && password === 'password') {
122
- const token = createJWT({ username, role: 'admin' }, 'your-secret-key', 3600);
123
- return { token, user: { username, role: 'admin' } };
124
- }
125
-
126
- ctx.set.status = 401;
127
- return { error: 'Invalid credentials' };
128
- }, {
129
- body: z.object({
130
- username: z.string(),
131
- password: z.string()
132
- })
133
- })
134
-
135
- .get('/protected', async (ctx) => {
136
- // ctx.user is available here because of auth plugin
137
- return { message: 'This is protected', user: ctx.user };
138
- })
139
-
140
- .get('/status', async (ctx) => {
141
- return {
142
- ...app.getServerInfo(),
143
- uptime: process.uptime(),
144
- memory: process.memoryUsage()
145
- };
146
- })
147
-
148
- .put('/users/:id', async (ctx) => {
149
- return {
150
- updated: ctx.body,
151
- id: ctx.params.id,
152
- version: ctx.headers['if-match']
153
- };
154
- }, {
155
- params: z.object({ id: z.string().uuid() }),
156
- body: z.object({
157
- name: z.string().optional(),
158
- email: z.string().email().optional()
159
- }),
160
- headers: z.object({
161
- 'if-match': z.string()
162
- })
163
- })
164
-
165
- .delete('/users/:id', async (ctx) => {
166
- ctx.set.status = 204;
167
- return null;
168
- }, {
169
- params: z.object({ id: z.string().uuid() })
170
- });
171
-
172
- // Start the server (with hot reload enabled)
173
- app.start(3000, 'localhost');
174
-
175
- console.log(`
176
- ๐ŸฆŠ BXO Framework with Hot Reload
177
-
178
- โœจ Features Enabled:
179
- - ๐ŸŽฃ Full lifecycle hooks (before/after pattern)
180
- - ๐Ÿ”’ JWT authentication
181
- - ๐Ÿ“Š Rate limiting
182
- - ๐ŸŒ CORS support
183
- - ๐Ÿ“ Request logging
184
- - ๐Ÿ”Œ API Plugin with routes
185
-
186
- ๐Ÿงช Try these endpoints:
187
- - GET /simple
188
- - GET /users/123e4567-e89b-12d3-a456-426614174000?include=profile
189
- - POST /users (with JSON body: {"name": "John", "email": "john@example.com"})
190
- - GET /health (shows server info)
191
- - POST /login (with JSON body: {"username": "admin", "password": "password"})
192
- - GET /protected (requires Bearer token from /login)
193
- - GET /status (server statistics)
194
-
195
- ๐Ÿ”Œ API Plugin endpoints:
196
- - GET /api/info (plugin information)
197
- - GET /api/ping (ping pong)
198
- - GET /api/time (current time)
199
- - POST /api/echo (echo message: {"message": "hello"})
200
-
201
- ๐Ÿ’ก Edit this file and save to see hot reload in action!
202
- `);
203
-
204
- console.log(app.routes)