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

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 CHANGED
@@ -3,6 +3,19 @@ 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
+ // Cookie interface
7
+ interface Cookie {
8
+ name: string;
9
+ value: string;
10
+ domain?: string;
11
+ path?: string;
12
+ expires?: Date;
13
+ maxAge?: number;
14
+ secure?: boolean;
15
+ httpOnly?: boolean;
16
+ sameSite?: 'Strict' | 'Lax' | 'None';
17
+ }
18
+
6
19
  // OpenAPI detail information
7
20
  interface RouteDetail {
8
21
  summary?: string;
@@ -21,6 +34,7 @@ interface RouteConfig {
21
34
  query?: z.ZodSchema<any>;
22
35
  body?: z.ZodSchema<any>;
23
36
  headers?: z.ZodSchema<any>;
37
+ cookies?: z.ZodSchema<any>;
24
38
  response?: z.ZodSchema<any>;
25
39
  detail?: RouteDetail;
26
40
  }
@@ -31,11 +45,13 @@ export type Context<TConfig extends RouteConfig = {}> = {
31
45
  query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
32
46
  body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
33
47
  headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
48
+ cookies: TConfig['cookies'] extends z.ZodSchema<any> ? InferZodType<TConfig['cookies']> : Record<string, string>;
34
49
  path: string;
35
50
  request: Request;
36
51
  set: {
37
52
  status?: number;
38
53
  headers?: Record<string, string>;
54
+ cookies?: Cookie[];
39
55
  };
40
56
  [key: string]: any;
41
57
  };
@@ -394,6 +410,23 @@ export default class BXO {
394
410
  return headerObj;
395
411
  }
396
412
 
413
+ // Parse cookies from Cookie header
414
+ private parseCookies(cookieHeader: string | null): Record<string, string> {
415
+ const cookies: Record<string, string> = {};
416
+
417
+ if (!cookieHeader) return cookies;
418
+
419
+ const cookiePairs = cookieHeader.split(';');
420
+ for (const pair of cookiePairs) {
421
+ const [name, value] = pair.trim().split('=');
422
+ if (name && value) {
423
+ cookies[decodeURIComponent(name)] = decodeURIComponent(value);
424
+ }
425
+ }
426
+
427
+ return cookies;
428
+ }
429
+
397
430
  // Validate data against Zod schema
398
431
  private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
399
432
  if (!schema) return data;
@@ -433,6 +466,7 @@ export default class BXO {
433
466
  const { route, params } = matchResult;
434
467
  const query = this.parseQuery(url.searchParams);
435
468
  const headers = this.parseHeaders(request.headers);
469
+ const cookies = this.parseCookies(request.headers.get('cookie'));
436
470
 
437
471
  let body: any;
438
472
  if (request.method !== 'GET' && request.method !== 'HEAD') {
@@ -470,12 +504,14 @@ export default class BXO {
470
504
  const validatedQuery = route.config?.query ? this.validateData(route.config.query, query) : query;
471
505
  const validatedBody = route.config?.body ? this.validateData(route.config.body, body) : body;
472
506
  const validatedHeaders = route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
507
+ const validatedCookies = route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
473
508
 
474
509
  ctx = {
475
510
  params: validatedParams,
476
511
  query: validatedQuery,
477
512
  body: validatedBody,
478
513
  headers: validatedHeaders,
514
+ cookies: validatedCookies,
479
515
  path: pathname,
480
516
  request,
481
517
  set: {}
@@ -602,9 +638,34 @@ export default class BXO {
602
638
  return new Response(bunFile, responseInit);
603
639
  }
604
640
 
641
+ // Prepare headers with cookies
642
+ let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
643
+
644
+ // Handle cookies if any are set
645
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
646
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
647
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
648
+
649
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
650
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
651
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
652
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
653
+ if (cookie.secure) cookieString += `; Secure`;
654
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
655
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
656
+
657
+ return cookieString;
658
+ });
659
+
660
+ // Add Set-Cookie headers
661
+ cookieHeaders.forEach((cookieHeader, index) => {
662
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
663
+ });
664
+ }
665
+
605
666
  const responseInit: ResponseInit = {
606
667
  status: ctx.set.status || 200,
607
- headers: ctx.set.headers || {}
668
+ headers: responseHeaders
608
669
  };
609
670
 
610
671
  if (typeof response === 'string') {
@@ -905,4 +966,15 @@ const file = (path: string, options?: { type?: string; headers?: Record<string,
905
966
  export { z, error, file };
906
967
 
907
968
  // Export types for external use
908
- export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
969
+ export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie };
970
+
971
+ // Helper function to create a cookie
972
+ export const createCookie = (
973
+ name: string,
974
+ value: string,
975
+ options: Omit<Cookie, 'name' | 'value'> = {}
976
+ ): Cookie => ({
977
+ name,
978
+ value,
979
+ ...options
980
+ });
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.21",
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)