bxo 0.0.5-dev.8 → 0.0.5-dev.81

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.
@@ -0,0 +1,132 @@
1
+ import BXO from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3000 } });
5
+
6
+ // HTTP routes
7
+ app.get("/", (ctx) => {
8
+ return ctx.text(`
9
+ <!DOCTYPE html>
10
+ <html>
11
+ <head>
12
+ <title>BXO WebSocket Example</title>
13
+ </head>
14
+ <body>
15
+ <h1>BXO WebSocket Example</h1>
16
+ <div id="messages"></div>
17
+ <input type="text" id="messageInput" placeholder="Type a message...">
18
+ <button onclick="sendMessage()">Send</button>
19
+ <button onclick="connect()">Connect</button>
20
+ <button onclick="disconnect()">Disconnect</button>
21
+
22
+ <script>
23
+ let ws = null;
24
+
25
+ function connect() {
26
+ ws = new WebSocket('ws://localhost:3000/ws');
27
+
28
+ ws.onopen = function() {
29
+ addMessage('Connected to WebSocket');
30
+ };
31
+
32
+ ws.onmessage = function(event) {
33
+ addMessage('Received: ' + event.data);
34
+ };
35
+
36
+ ws.onclose = function() {
37
+ addMessage('Disconnected from WebSocket');
38
+ };
39
+
40
+ ws.onerror = function(error) {
41
+ addMessage('Error: ' + error);
42
+ };
43
+ }
44
+
45
+ function disconnect() {
46
+ if (ws) {
47
+ ws.close();
48
+ ws = null;
49
+ }
50
+ }
51
+
52
+ function sendMessage() {
53
+ const input = document.getElementById('messageInput');
54
+ if (ws && input.value) {
55
+ ws.send(input.value);
56
+ addMessage('Sent: ' + input.value);
57
+ input.value = '';
58
+ }
59
+ }
60
+
61
+ function addMessage(message) {
62
+ const messages = document.getElementById('messages');
63
+ const div = document.createElement('div');
64
+ div.textContent = new Date().toLocaleTimeString() + ': ' + message;
65
+ messages.appendChild(div);
66
+ messages.scrollTop = messages.scrollHeight;
67
+ }
68
+
69
+ // Allow Enter key to send message
70
+ document.getElementById('messageInput').addEventListener('keypress', function(e) {
71
+ if (e.key === 'Enter') {
72
+ sendMessage();
73
+ }
74
+ });
75
+ </script>
76
+ `, 200, {
77
+ "Content-Type": "text/html"
78
+ });
79
+ });
80
+
81
+ // WebSocket route
82
+ app.ws("/ws", {
83
+ open(ws) {
84
+ console.log("WebSocket connection opened");
85
+ ws.send("Welcome to BXO WebSocket!");
86
+ },
87
+
88
+ message(ws, message) {
89
+ console.log("Received message:", message);
90
+ // Echo the message back
91
+ ws.send(`Echo: ${message}`);
92
+ },
93
+
94
+ close(ws, code, reason) {
95
+ console.log(`WebSocket connection closed: ${code} ${reason}`);
96
+ },
97
+
98
+ ping(ws, data) {
99
+ console.log("Ping received:", data);
100
+ },
101
+
102
+ pong(ws, data) {
103
+ console.log("Pong received:", data);
104
+ }
105
+ });
106
+
107
+ // Another WebSocket route with parameters
108
+ app.ws("/chat/:room", {
109
+ open(ws) {
110
+ console.log(`WebSocket connection opened for room: ${ws.data?.room || 'unknown'}`);
111
+ ws.send(`Welcome to chat room: ${ws.data?.room || 'unknown'}`);
112
+ },
113
+
114
+ message(ws, message) {
115
+ const room = ws.data?.room || 'unknown';
116
+ console.log(`Message in room ${room}:`, message);
117
+ ws.send(`[${room}] Echo: ${message}`);
118
+ },
119
+
120
+ close(ws, code, reason) {
121
+ const room = ws.data?.room || 'unknown';
122
+ console.log(`WebSocket connection closed for room ${room}: ${code} ${reason}`);
123
+ }
124
+ });
125
+
126
+ app.start();
127
+ console.log(`Server is running on http://localhost:${app.server?.port}`);
128
+ console.log(`WebSocket available at ws://localhost:${app.server?.port}/ws`);
129
+ console.log(`Chat WebSocket available at ws://localhost:${app.server?.port}/chat/:room`);
130
+ }
131
+
132
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "bxo",
3
- "module": "index.ts",
4
- "version": "0.0.5-dev.8",
5
- "description": "A simple and lightweight web framework for Bun",
3
+ "module": "./src/index.ts",
4
+ "exports": {
5
+ ".": "./src/index.ts",
6
+ "./plugins": "./plugins/index.ts"
7
+ },
8
+ "version": "0.0.5-dev.81",
6
9
  "type": "module",
7
10
  "devDependencies": {
8
11
  "@types/bun": "latest"
9
12
  },
10
- "exports": {
11
- ".": "./index.ts",
12
- "./plugins": "./plugins/index.ts"
13
- },
14
13
  "peerDependencies": {
15
14
  "typescript": "^5"
16
15
  },
17
16
  "dependencies": {
18
- "zod": "^4.0.5"
17
+ "zod": "^4.1.5",
18
+ "zod-openapi": "^5.4.0"
19
19
  }
20
20
  }
package/plugins/cors.ts CHANGED
@@ -1,83 +1,133 @@
1
- import BXO from '../index';
2
-
3
- interface CORSOptions {
4
- origin?: string | string[] | boolean;
5
- methods?: string[];
6
- allowedHeaders?: string[];
7
- credentials?: boolean;
8
- maxAge?: number;
1
+ import BXO from "../src/index";
2
+
3
+ export interface CorsOptions {
4
+ origin?: string | string[] | boolean | ((origin: string) => boolean);
5
+ methods?: string[];
6
+ allowedHeaders?: string[];
7
+ exposedHeaders?: string[];
8
+ credentials?: boolean;
9
+ maxAge?: number;
10
+ preflightContinue?: boolean;
9
11
  }
10
12
 
11
- export function cors(options: CORSOptions = {}): BXO {
12
- const {
13
- origin = '*',
14
- methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
15
- allowedHeaders = ['Content-Type', 'Authorization'],
16
- credentials = false,
17
- maxAge = 86400
18
- } = options;
19
-
20
- const corsInstance = new BXO();
21
-
22
- corsInstance.onRequest(async (ctx: any) => {
23
- // Handle preflight OPTIONS request
24
- if (ctx.request.method === 'OPTIONS') {
25
- const headers: Record<string, string> = {};
26
-
27
- // Handle origin
28
- if (typeof origin === 'boolean') {
29
- if (origin) {
30
- headers['Access-Control-Allow-Origin'] = ctx.request.headers.get('origin') || '*';
13
+ export function cors(options: CorsOptions = {}): BXO {
14
+ const {
15
+ origin = "*",
16
+ methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
17
+ allowedHeaders = ["Content-Type", "Authorization"],
18
+ exposedHeaders = [],
19
+ credentials = false,
20
+ maxAge = 86400,
21
+ preflightContinue = false
22
+ } = options;
23
+
24
+ const plugin = new BXO();
25
+
26
+ // Handle CORS preflight requests
27
+ plugin.beforeRequest(async (req) => {
28
+ const requestOrigin = req.headers.get("origin");
29
+ const requestMethod = req.method;
30
+
31
+ // Handle preflight OPTIONS request
32
+ if (requestMethod === "OPTIONS") {
33
+ const response = new Response(null, { status: 204 });
34
+
35
+ // Set CORS headers
36
+ setCorsHeaders(response, {
37
+ origin,
38
+ methods,
39
+ allowedHeaders,
40
+ exposedHeaders,
41
+ credentials,
42
+ maxAge,
43
+ requestOrigin
44
+ });
45
+
46
+ if (!preflightContinue) {
47
+ return response;
48
+ }
31
49
  }
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;
50
+
51
+ // Continue with normal request processing
52
+ return req;
53
+ });
54
+
55
+ // Add CORS headers to all responses
56
+ plugin.afterRequest(async (req, res) => {
57
+ const requestOrigin = req.headers.get("origin");
58
+
59
+ // Set CORS headers on the response
60
+ setCorsHeaders(res, {
61
+ origin,
62
+ methods,
63
+ allowedHeaders,
64
+ exposedHeaders,
65
+ credentials,
66
+ maxAge,
67
+ requestOrigin
68
+ });
69
+
70
+ return res;
71
+ });
72
+
73
+ return plugin;
74
+ }
75
+
76
+ function setCorsHeaders(
77
+ response: Response,
78
+ options: {
79
+ origin: string | string[] | boolean | ((origin: string) => boolean);
80
+ methods: string[];
81
+ allowedHeaders: string[];
82
+ exposedHeaders: string[];
83
+ credentials: boolean;
84
+ maxAge: number;
85
+ requestOrigin?: string | null;
86
+ }
87
+ ) {
88
+ const { origin, methods, allowedHeaders, exposedHeaders, credentials, maxAge, requestOrigin } = options;
89
+
90
+ // Set Access-Control-Allow-Origin
91
+ if (origin) {
92
+ if (typeof origin === "string") {
93
+ if (origin === "*") {
94
+ response.headers.set("Access-Control-Allow-Origin", "*");
95
+ } else {
96
+ response.headers.set("Access-Control-Allow-Origin", origin);
97
+ }
98
+ } else if (Array.isArray(origin)) {
99
+ // For array of origins, we need to check if the request origin is in the list
100
+ // This is handled in the beforeRequest hook where we have access to the request origin
101
+ if (options.requestOrigin && origin.includes(options.requestOrigin)) {
102
+ response.headers.set("Access-Control-Allow-Origin", options.requestOrigin);
103
+ }
104
+ } else if (origin === true) {
105
+ response.headers.set("Access-Control-Allow-Origin", "*");
38
106
  }
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
107
  }
55
- });
56
-
57
- corsInstance.onResponse(async (ctx: any, response: any) => {
58
- const headers: Record<string, string> = {};
59
-
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') || '*';
64
- }
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
- }
108
+
109
+ // Set Access-Control-Allow-Methods
110
+ if (methods.length > 0) {
111
+ response.headers.set("Access-Control-Allow-Methods", methods.join(", "));
72
112
  }
73
113
 
74
- if (credentials) {
75
- headers['Access-Control-Allow-Credentials'] = 'true';
114
+ // Set Access-Control-Allow-Headers
115
+ if (allowedHeaders.length > 0) {
116
+ response.headers.set("Access-Control-Allow-Headers", allowedHeaders.join(", "));
76
117
  }
77
118
 
78
- ctx.set.headers = { ...ctx.set.headers, ...headers };
79
- return response;
80
- });
119
+ // Set Access-Control-Expose-Headers
120
+ if (exposedHeaders.length > 0) {
121
+ response.headers.set("Access-Control-Expose-Headers", exposedHeaders.join(", "));
122
+ }
81
123
 
82
- return corsInstance;
83
- }
124
+ // Set Access-Control-Allow-Credentials
125
+ if (credentials) {
126
+ response.headers.set("Access-Control-Allow-Credentials", "true");
127
+ }
128
+
129
+ // Set Access-Control-Max-Age
130
+ if (maxAge) {
131
+ response.headers.set("Access-Control-Max-Age", maxAge.toString());
132
+ }
133
+ }
package/plugins/index.ts CHANGED
@@ -1,11 +1,2 @@
1
- // Export all plugins
2
- export { cors } from './cors';
3
- export { logger } from './logger';
4
- export { auth, createJWT } from './auth';
5
- export { rateLimit } from './ratelimit';
6
-
7
- // Import BXO for plugin typing
8
- import BXO from '../index';
9
-
10
- // Plugin functions now return BXO instances
11
- export type PluginFactory<T = any> = (options?: T) => BXO;
1
+ export * from "./openapi";
2
+ export * from "./cors";
@@ -0,0 +1,204 @@
1
+ import BXO, { z } from "../src";
2
+ import { createDocument, type CreateDocumentOptions, type ZodOpenApiPathItemObject, type ZodOpenApiPathsObject, type ZodOpenApiSecuritySchemeObject } from "zod-openapi";
3
+
4
+ interface SecurityScheme extends ZodOpenApiSecuritySchemeObject {
5
+ type: "http" | "apiKey" | "oauth2" | "openIdConnect";
6
+ scheme?: "bearer" | "basic" | "digest" | "apikey";
7
+ bearerFormat?: string;
8
+ description?: string;
9
+ name?: string;
10
+ in?: "header" | "query" | "cookie";
11
+ }
12
+
13
+ interface OpenApiPluginConfig {
14
+ path: string;
15
+ jsonPath: string;
16
+ openapiConfig: CreateDocumentOptions;
17
+ defaultTags?: string[];
18
+ securitySchemes?: Record<string, SecurityScheme>;
19
+ globalSecurity?: Array<Record<string, string[]>>;
20
+ }
21
+
22
+ const createOpenApiPaths = (app: BXO, config?: OpenApiPluginConfig): ZodOpenApiPathsObject => {
23
+ const routes = app.getRoutes()
24
+ let paths: ZodOpenApiPathsObject = {}
25
+ for (const route of routes) {
26
+ const openapiPath = "/" + route.path.replace(/:(\w+)/g, "{$1}").replace(/\*/g, "*").replace("/", "")
27
+ const method = route.method.toLowerCase()
28
+ if (method === "default") {
29
+ continue
30
+ }
31
+ const contentType = route.schema?.detail?.defaultContentType || "application/json"
32
+ if (config?.path && openapiPath === config?.path) {
33
+ continue
34
+ }
35
+ if (config?.jsonPath && openapiPath === config?.jsonPath) {
36
+ continue
37
+ }
38
+ if (route.schema?.detail?.hidden) {
39
+ continue
40
+ }
41
+
42
+ // Extract tags from route metadata
43
+ const tags = route.schema?.detail?.tags ||
44
+ route.schema?.detail?.tag ||
45
+ config?.defaultTags ||
46
+ []
47
+
48
+ // Extract security requirements from route metadata
49
+ const routeSecurity = route.schema?.detail?.security ||
50
+ route.schema?.detail?.auth ||
51
+ undefined
52
+
53
+ // Extract operation summary and description
54
+ const summary = route.schema?.detail?.summary ||
55
+ route.schema?.detail?.title ||
56
+ `${method.toUpperCase()} ${route.path}`
57
+
58
+ const description = route.schema?.detail?.description ||
59
+ route.schema?.detail?.docs ||
60
+ undefined
61
+
62
+ // Extract parameters from route path
63
+ const parameters = []
64
+ const pathParams = route.path.match(/:\w+/g)
65
+ if (pathParams) {
66
+ for (const param of pathParams) {
67
+ const paramName = param.slice(1) // Remove the colon
68
+ const paramSchema = route.schema?.detail?.params?.[paramName] || z.string()
69
+ parameters.push({
70
+ name: paramName,
71
+ in: "path",
72
+ required: true,
73
+ schema: paramSchema
74
+ })
75
+ }
76
+ }
77
+
78
+ // Add query parameters if defined
79
+ if (route.schema?.query) {
80
+ const querySchema = route.schema?.query
81
+ if (querySchema && typeof querySchema === 'object' && 'shape' in querySchema) {
82
+ const queryShape = (querySchema as any).shape
83
+ for (const [key, schema] of Object.entries(queryShape)) {
84
+ const isOptional = schema instanceof z.ZodOptional
85
+ parameters.push({
86
+ name: key,
87
+ in: "query",
88
+ required: !isOptional, // Query params are typically optional
89
+ schema: schema as any
90
+ })
91
+ }
92
+ }
93
+ }
94
+
95
+ const response = Object.entries(route.schema?.response || {}).map(([status, schema]) => {
96
+ return ({
97
+ 400: status === "400" && !route.schema?.response?.[status] ? {
98
+ content: {
99
+ "application/json": {
100
+ schema: z.object({
101
+ error: z.string(),
102
+ issues: z.any().optional()
103
+ })
104
+ }
105
+ }
106
+ } : undefined,
107
+ [status]: {
108
+ content: {
109
+ "application/json": {
110
+ schema: schema
111
+ }
112
+ }
113
+ }
114
+ })
115
+ }).reduce((acc, curr) => ({ ...acc, ...curr }), {})
116
+
117
+ paths[openapiPath] = {
118
+ ...paths[openapiPath],
119
+ [method]: {
120
+ tags: tags.length > 0 ? tags : undefined,
121
+ summary: summary,
122
+ description: description,
123
+ parameters: parameters.length > 0 ? parameters : undefined,
124
+ security: routeSecurity,
125
+ requestBody: {
126
+ content: {
127
+ [contentType]: {
128
+ schema: route.schema?.body || z.object({})
129
+ }
130
+ }
131
+ },
132
+ responses: response || {
133
+ 200: {
134
+ content: {
135
+ "application/json": {
136
+ schema: z.object({})
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ } satisfies ZodOpenApiPathItemObject
143
+ }
144
+ return paths
145
+ }
146
+
147
+ export function openapi(_config?: OpenApiPluginConfig) {
148
+ let config = _config
149
+ !config && (config = { path: "/docs", openapiConfig: {}, jsonPath: "/openapi.json" })
150
+ config.path = config.path || "/docs"
151
+ config.jsonPath = config.jsonPath || "/openapi.json"
152
+ config.openapiConfig = config.openapiConfig || {}
153
+ config.defaultTags = config.defaultTags || []
154
+ config.securitySchemes = config.securitySchemes || {}
155
+ config.globalSecurity = config.globalSecurity || []
156
+
157
+ const bxo = new BXO()
158
+ .get(config.jsonPath, (ctx, app) => {
159
+ const paths = createDocument({
160
+ openapi: "3.0.0",
161
+ info: {
162
+ title: "My API",
163
+ version: "1.0.0"
164
+ },
165
+ paths: createOpenApiPaths(app, config),
166
+ components: {
167
+ securitySchemes: Object.keys(config.securitySchemes || {}).length > 0 ? config.securitySchemes : undefined
168
+ },
169
+ security: (config.globalSecurity || []).length > 0 ? config.globalSecurity : undefined,
170
+ ...config.openapiConfig
171
+ })
172
+ return new Response(JSON.stringify(paths), {
173
+ headers: {
174
+ "Content-Type": "application/json"
175
+ }
176
+ })
177
+ })
178
+ .get(config.path, (ctx, app) => {
179
+ ctx.set.headers["Content-Type"] = "text/html"
180
+ return `
181
+ <!doctype html>
182
+ <html>
183
+ <head>
184
+ <title>My Scalar API Reference</title>
185
+ <meta charset="utf-8" />
186
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
187
+ </head>
188
+ <body>
189
+ <div id="app"></div>
190
+ <!-- Load Scalar -->
191
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
192
+ <!-- Initialize Scalar -->
193
+ <script>
194
+ Scalar.createApiReference('#app', {
195
+ url: '/openapi.json',
196
+ proxyUrl: 'https://proxy.scalar.com'
197
+ })
198
+ </script>
199
+ </body>
200
+ </html>`
201
+ })
202
+
203
+ return bxo
204
+ }