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.
- package/README.md +83 -675
- package/example/cors-example.ts +49 -0
- package/example/index.html +5 -0
- package/example/index.ts +57 -0
- package/package.json +9 -15
- package/plugins/cors.ts +124 -98
- package/plugins/index.ts +2 -9
- package/plugins/openapi.ts +130 -0
- package/src/index.ts +646 -59
- package/tsconfig.json +3 -5
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/examples/serve-react/README.md +0 -15
- package/examples/serve-react/app.tsx +0 -8
- package/examples/serve-react/bun.lock +0 -42
- package/examples/serve-react/index.html +0 -9
- package/examples/serve-react/index.ts +0 -27
- package/examples/serve-react/package.json +0 -17
- package/examples/serve-react/tsconfig.json +0 -29
- package/index.ts +0 -5
- package/plugins/README.md +0 -160
- package/plugins/ratelimit.ts +0 -136
- package/src/core/bxo.ts +0 -458
- package/src/handlers/request-handler.ts +0 -230
- package/src/types/index.ts +0 -167
- package/src/utils/context-factory.ts +0 -158
- package/src/utils/helpers.ts +0 -40
- package/src/utils/index.ts +0 -448
- package/src/utils/response-handler.ts +0 -293
- package/src/utils/route-matcher.ts +0 -191
- package/tests/README.md +0 -359
- package/tests/integration/bxo.test.ts +0 -616
- package/tests/run-tests.ts +0 -44
- package/tests/unit/context-factory.test.ts +0 -386
- package/tests/unit/helpers.test.ts +0 -253
- package/tests/unit/response-handler.test.ts +0 -327
- package/tests/unit/route-matcher.test.ts +0 -181
- 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!");
|
package/example/index.ts
ADDED
|
@@ -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": "^
|
|
16
|
-
"
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
32
|
-
}
|
|
24
|
+
const plugin = new BXO();
|
|
33
25
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
2
|
-
export
|
|
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
|
+
}
|