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.
- package/README.md +281 -480
- package/example/cookie-example.ts +151 -0
- package/example/cors-example.ts +49 -0
- package/example/index.html +5 -0
- package/example/index.ts +191 -0
- package/example/multipart-example.ts +322 -0
- package/example/openapi-example.ts +132 -0
- package/example/passthrough-validation-example.ts +115 -0
- package/example/url-encoding-example.ts +93 -0
- package/example/websocket-example.ts +132 -0
- package/package.json +8 -8
- package/plugins/cors.ts +123 -73
- package/plugins/index.ts +2 -11
- package/plugins/openapi.ts +204 -0
- package/src/index.ts +989 -0
- package/test-url-encoding.ts +20 -0
- package/tsconfig.json +3 -5
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/example.ts +0 -183
- package/index.ts +0 -835
- package/plugins/auth.ts +0 -119
- package/plugins/logger.ts +0 -109
- package/plugins/ratelimit.ts +0 -140
|
@@ -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
|
-
"
|
|
5
|
-
|
|
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.
|
|
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
|
|
2
|
-
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
export
|
|
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
|
+
}
|