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 +74 -2
- package/package.json +1 -1
- package/plugins/index.ts +0 -2
- package/example.ts +0 -204
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:
|
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
package/plugins/index.ts
CHANGED
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)
|