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
@@ -0,0 +1,49 @@
1
+ import BXO from "../src/index";
2
+ import { cors } from "../plugins";
3
+
4
+ const app = new BXO();
5
+
6
+ // Use the CORS plugin
7
+ app.use(cors({
8
+ origin: ["http://localhost:3000", "http://localhost:3001"],
9
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
10
+ allowedHeaders: ["Content-Type", "Authorization"],
11
+ credentials: true
12
+ }));
13
+
14
+ // Add some routes
15
+ app.get("/api/users", async (ctx) => {
16
+ return ctx.json([
17
+ { id: 1, name: "John Doe" },
18
+ { id: 2, name: "Jane Smith" }
19
+ ]);
20
+ });
21
+
22
+ app.post("/api/users", async (ctx) => {
23
+ const user = ctx.body as { name: string };
24
+ return ctx.json({ id: 3, name: user.name }, 201);
25
+ });
26
+
27
+ // Custom beforeRequest hook example
28
+ app.beforeRequest(async (req) => {
29
+ console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
30
+ return req; // Continue with the request
31
+ });
32
+
33
+ // Custom afterRequest hook example
34
+ app.afterRequest(async (req, res) => {
35
+ console.log(`[${new Date().toISOString()}] Response: ${res.status}`);
36
+ return res; // Return the modified response
37
+ });
38
+
39
+ // Custom error handler
40
+ app.onError(async (error, req) => {
41
+ console.error(`Error handling ${req.method} ${req.url}:`, error);
42
+ return new Response("Something went wrong", { status: 500 });
43
+ });
44
+
45
+ // Start the server
46
+ app.start();
47
+
48
+ console.log("Server running on http://localhost:3000");
49
+ console.log("Try making a CORS request from another origin!");
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ <h1>Hello, world!</h1>
4
+ </body>
5
+ </html>
@@ -0,0 +1,57 @@
1
+ import BXO, { z } from "../src";
2
+ import index from "./index.html";
3
+ import openapi from "../plugins/openapi";
4
+
5
+ async function main() {
6
+ const bxo = new BXO();
7
+
8
+ bxo.default("/", index);
9
+
10
+ bxo.get("/:id", (ctx) => {
11
+ return new Response(ctx.params.id + ctx.query.name, {
12
+ headers: {
13
+ "Content-Type": "text/html"
14
+ }
15
+ });
16
+ }, {
17
+ query: z.object({
18
+ name: z.number()
19
+ }),
20
+ response: {
21
+ 200: z.object({
22
+ name: z.string()
23
+ })
24
+ },
25
+ });
26
+ bxo.post("/", (ctx) => {
27
+ console.log(ctx.body)
28
+ return new Response("Hello" + ctx.body.name, {
29
+ headers: {
30
+ "Content-Type": "text/html"
31
+ }
32
+ });
33
+ }, {
34
+ detail: {
35
+ defaultContentType: "multipart/form-data"
36
+
37
+ },
38
+ body: z.object({
39
+ name: z.string(),
40
+ avatar: z.file()
41
+ }),
42
+ response: {
43
+ 200: z.object({
44
+ name: z.string()
45
+ }),
46
+ 400: z.object({
47
+ error: z.string()
48
+ })
49
+ }
50
+ });
51
+ bxo.use(openapi())
52
+ bxo.start();
53
+ console.log(`Server is running on http://localhost:${bxo.server?.port}`);
54
+
55
+ }
56
+
57
+ main();
package/package.json CHANGED
@@ -1,26 +1,20 @@
1
1
  {
2
2
  "name": "bxo",
3
- "module": "index.ts",
4
- "version": "0.0.5-dev.65",
5
- "description": "A simple and lightweight web framework for Bun",
6
- "type": "module",
3
+ "module": "./src/index.ts",
7
4
  "exports": {
8
- ".": "./index.ts",
5
+ ".": "./src/index.ts",
9
6
  "./plugins": "./plugins/index.ts"
10
7
  },
8
+ "version": "0.0.5-dev.66",
9
+ "type": "module",
10
+ "devDependencies": {
11
+ "@types/bun": "latest"
12
+ },
11
13
  "peerDependencies": {
12
14
  "typescript": "^5"
13
15
  },
14
16
  "dependencies": {
15
- "zod": "^3.25.76",
16
- "@types/bun": "latest"
17
- },
18
- "scripts": {
19
- "test": "bun test",
20
- "test:unit": "bun test tests/unit/",
21
- "test:integration": "bun test tests/integration/",
22
- "test:all": "bun run tests/run-tests.ts",
23
- "test:watch": "bun test --watch",
24
- "test:coverage": "bun test --coverage"
17
+ "zod": "^4.1.5",
18
+ "zod-openapi": "^5.4.0"
25
19
  }
26
20
  }
package/plugins/cors.ts CHANGED
@@ -1,107 +1,133 @@
1
- import type { Plugin } 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
- // Helper function to extract origin from request headers
12
- function getRequestOrigin(request: Request): string | null {
13
- // Try Origin header first (standard for CORS)
14
- const origin = request.headers.get('origin');
15
- if (origin) {
16
- return origin;
17
- }
18
-
19
- // Fallback to Referer header
20
- const referer = request.headers.get('referer');
21
- if (referer) {
22
- try {
23
- const url = new URL(referer);
24
- return `${url.protocol}//${url.host}`;
25
- } catch (e) {
26
- // Invalid referer URL, ignore it
27
- return null;
28
- }
29
- }
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;
30
23
 
31
- return null;
32
- }
24
+ const plugin = new BXO();
33
25
 
34
- // Helper function to validate origin against allowed origins
35
- function validateOrigin(requestOrigin: string | null, allowedOrigins: string | string[] | boolean): string | null {
36
- if (!requestOrigin) {
37
- return null;
38
- }
39
-
40
- if (typeof allowedOrigins === 'boolean') {
41
- return allowedOrigins ? requestOrigin : null;
42
- } else if (typeof allowedOrigins === 'string') {
43
- return allowedOrigins === '*' || allowedOrigins === requestOrigin ? requestOrigin : null;
44
- } else if (Array.isArray(allowedOrigins)) {
45
- return allowedOrigins.includes(requestOrigin) ? requestOrigin : null;
46
- }
47
-
48
- return null;
49
- }
26
+ // Handle CORS preflight requests
27
+ plugin.beforeRequest(async (req) => {
28
+ const requestOrigin = req.headers.get("origin");
29
+ const requestMethod = req.method;
50
30
 
51
- export function cors(options: CORSOptions = {}): Plugin {
52
- const {
53
- origin = '*',
54
- methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
55
- allowedHeaders = ['Content-Type', 'Authorization'],
56
- credentials = false,
57
- maxAge = 86400
58
- } = options;
59
-
60
- return {
61
- name: 'cors',
62
- onRequest: async (ctx) => {
63
- // Handle preflight OPTIONS requests
64
- if (ctx.request.method === 'OPTIONS') {
65
- const requestOrigin = getRequestOrigin(ctx.request);
66
- const allowedOrigin = validateOrigin(requestOrigin, origin);
67
-
68
- // Set CORS headers for preflight
69
- ctx.set.headers = {
70
- ...ctx.set.headers,
71
- 'Access-Control-Allow-Origin': allowedOrigin || '*',
72
- 'Access-Control-Allow-Methods': methods.join(', '),
73
- 'Access-Control-Allow-Headers': allowedHeaders.join(', '),
74
- 'Access-Control-Max-Age': maxAge.toString()
75
- };
76
-
77
- if (credentials) {
78
- ctx.set.headers['Access-Control-Allow-Credentials'] = 'true';
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
+ }
79
49
  }
80
50
 
81
- // Return a proper Response for OPTIONS requests
82
- return new Response(null, {
83
- status: 204,
84
- headers: ctx.set.headers
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
85
68
  });
86
- }
87
- },
88
- onResponse: async (ctx) => {
89
- // Handle CORS headers for actual requests
90
- const requestOrigin = getRequestOrigin(ctx.request);
91
- const allowedOrigin = validateOrigin(requestOrigin, origin);
92
-
93
- // Set CORS headers for all responses
94
- ctx.set.headers = {
95
- ...ctx.set.headers,
96
- 'Access-Control-Allow-Origin': allowedOrigin || '*',
97
- 'Access-Control-Allow-Methods': methods.join(', '),
98
- 'Access-Control-Allow-Headers': allowedHeaders.join(', '),
99
- 'Access-Control-Max-Age': maxAge.toString()
100
- };
101
-
102
- if (credentials) {
103
- ctx.set.headers['Access-Control-Allow-Credentials'] = 'true';
104
- }
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", "*");
106
+ }
107
+ }
108
+
109
+ // Set Access-Control-Allow-Methods
110
+ if (methods.length > 0) {
111
+ response.headers.set("Access-Control-Allow-Methods", methods.join(", "));
105
112
  }
106
- };
107
- }
113
+
114
+ // Set Access-Control-Allow-Headers
115
+ if (allowedHeaders.length > 0) {
116
+ response.headers.set("Access-Control-Allow-Headers", allowedHeaders.join(", "));
117
+ }
118
+
119
+ // Set Access-Control-Expose-Headers
120
+ if (exposedHeaders.length > 0) {
121
+ response.headers.set("Access-Control-Expose-Headers", exposedHeaders.join(", "));
122
+ }
123
+
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,9 +1,2 @@
1
- // Export all plugins
2
- export { cors } from './cors';
3
- export { rateLimit } from './ratelimit';
4
-
5
- // Import types for plugin typing
6
- import type { Plugin } from '../index';
7
-
8
- // Plugin functions return Plugin instances
9
- export type PluginFactory<T = any> = (options?: T) => Plugin;
1
+ export * from "./openapi";
2
+ export * from "./cors";
@@ -0,0 +1,130 @@
1
+ import BXO, { z } from "../src";
2
+ import { createDocument, type CreateDocumentOptions, type ZodOpenApiPathsObject } from "zod-openapi";
3
+
4
+ class OpenApiConfig {
5
+
6
+ }
7
+
8
+ interface OpenApiPluginConfig {
9
+ path: string;
10
+ jsonPath: string;
11
+ openapiConfig: CreateDocumentOptions;
12
+ }
13
+
14
+ const createOpenApiPaths = (app: BXO, config?: OpenApiPluginConfig): ZodOpenApiPathsObject => {
15
+ const routes = app.getRoutes()
16
+ let paths: ZodOpenApiPathsObject = {}
17
+ for (const route of routes) {
18
+ const openapiPath = "/" + route.path.replace(/:(\w+)/g, "{$1}").replace(/\*/g, "*").replace(/\//g, "")
19
+ const method = route.method.toLowerCase()
20
+ if (method === "default") {
21
+ continue
22
+ }
23
+ const contentType = route.schema?.detail?.defaultContentType || "application/json"
24
+ if (config?.path && openapiPath === config?.path) {
25
+ continue
26
+ }
27
+ if (config?.jsonPath && openapiPath === config?.jsonPath) {
28
+ continue
29
+ }
30
+ if (route.schema?.detail?.hidden) {
31
+ continue
32
+ }
33
+ const response = Object.entries(route.schema?.response || {}).map(([status, schema]) => {
34
+ return ({
35
+ 400: status === "400" && !route.schema?.response?.[status] ? {
36
+ content: {
37
+ "application/json": {
38
+ schema: z.object({
39
+ error: z.string(),
40
+ issues: z.any().optional()
41
+ })
42
+ }
43
+ }
44
+ } : undefined,
45
+ [status]: {
46
+ content: {
47
+ "application/json": {
48
+ schema: schema
49
+ }
50
+ }
51
+ }
52
+ })
53
+ }).reduce((acc, curr) => ({ ...acc, ...curr }), {})
54
+ paths[openapiPath] = {
55
+ ...paths[openapiPath],
56
+ [method]: {
57
+ requestBody: {
58
+ content: {
59
+ [contentType]: {
60
+ schema: route.schema?.body || z.object({})
61
+ }
62
+ }
63
+ },
64
+ responses: response || {
65
+ 200: {
66
+ content: {
67
+ "application/json": {
68
+ schema: z.object({})
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return paths
77
+ }
78
+
79
+ export default function openapi(_config?: OpenApiPluginConfig) {
80
+ let config = _config
81
+ !config && (config = { path: "/docs", openapiConfig: new OpenApiConfig(), jsonPath: "/openapi.json" })
82
+ config.path = config.path || "/docs"
83
+ config.jsonPath = config.jsonPath || "/openapi.json"
84
+ config.openapiConfig = config.openapiConfig || {}
85
+
86
+
87
+ const bxo = new BXO()
88
+ .get(config.jsonPath, (ctx, app) => {
89
+ const paths = createDocument({
90
+ openapi: "3.0.0",
91
+ info: {
92
+ title: "My API",
93
+ version: "1.0.0"
94
+ },
95
+ paths: createOpenApiPaths(app, config),
96
+ ...config.openapiConfig
97
+ })
98
+ return new Response(JSON.stringify(paths), {
99
+ headers: {
100
+ "Content-Type": "application/json"
101
+ }
102
+ })
103
+ })
104
+ .get(config.path, (ctx, app) => {
105
+ ctx.set.headers["Content-Type"] = "text/html"
106
+ return `
107
+ <!doctype html>
108
+ <html>
109
+ <head>
110
+ <title>My Scalar API Reference</title>
111
+ <meta charset="utf-8" />
112
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
113
+ </head>
114
+ <body>
115
+ <div id="app"></div>
116
+ <!-- Load Scalar -->
117
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
118
+ <!-- Initialize Scalar -->
119
+ <script>
120
+ Scalar.createApiReference('#app', {
121
+ url: '/openapi.json',
122
+ proxyUrl: 'https://proxy.scalar.com'
123
+ })
124
+ </script>
125
+ </body>
126
+ </html>`
127
+ })
128
+
129
+ return bxo
130
+ }