xypriss-swagger 1.0.0
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/.env +1 -0
- package/bun.lock +530 -0
- package/package.json +18 -0
- package/src/configs/Logger.ts +211 -0
- package/src/configs/meta.ts +12 -0
- package/src/index.ts +35 -0
- package/src/openapi.ts +104 -0
- package/src/server.ts +94 -0
- package/src/template/ui.html +381 -0
- package/src/types.ts +25 -0
- package/src/ui.ts +26 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>{{title}} - XyPriss API Docs</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #05070a;
|
|
16
|
+
--surface: #0f1218;
|
|
17
|
+
--surface-hover: #161b22;
|
|
18
|
+
--primary: #6366f1;
|
|
19
|
+
--primary-hover: #818cf8;
|
|
20
|
+
--text: #f1f5f9;
|
|
21
|
+
--text-muted: #94a3b8;
|
|
22
|
+
--border: rgba(255, 255, 255, 0.06);
|
|
23
|
+
|
|
24
|
+
--get: #10b981;
|
|
25
|
+
--post: #3b82f6;
|
|
26
|
+
--put: #f59e0b;
|
|
27
|
+
--delete: #ef4444;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
* {
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 0;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
}
|
|
35
|
+
body {
|
|
36
|
+
font-family: "Outfit", sans-serif;
|
|
37
|
+
background-color: var(--bg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
display: flex;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
aside {
|
|
44
|
+
width: 320px;
|
|
45
|
+
background-color: var(--surface);
|
|
46
|
+
border-right: 1px solid var(--border);
|
|
47
|
+
padding: 2.5rem 1.5rem;
|
|
48
|
+
position: sticky;
|
|
49
|
+
top: 0;
|
|
50
|
+
height: 100vh;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
overflow-y: auto;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.logo {
|
|
57
|
+
font-size: 1.25rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
margin-bottom: 2.5rem;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 0.75rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.logo-box {
|
|
66
|
+
width: 32px;
|
|
67
|
+
height: 32px;
|
|
68
|
+
background: linear-gradient(135deg, var(--primary), #a855f7);
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.nav-group-title {
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
font-size: 0.7rem;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
letter-spacing: 0.1em;
|
|
77
|
+
color: var(--text-muted);
|
|
78
|
+
margin-bottom: 1rem;
|
|
79
|
+
margin-top: 2rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.nav-item {
|
|
83
|
+
display: block;
|
|
84
|
+
padding: 0.75rem 1rem;
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
color: var(--text-muted);
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
font-size: 0.9rem;
|
|
89
|
+
transition: all 0.2s;
|
|
90
|
+
margin-bottom: 0.25rem;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-overflow: ellipsis;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.nav-item:hover {
|
|
97
|
+
background: var(--surface-hover);
|
|
98
|
+
color: var(--text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.nav-item.active {
|
|
102
|
+
background: rgba(99, 102, 241, 0.1);
|
|
103
|
+
color: var(--primary);
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main {
|
|
108
|
+
flex: 1;
|
|
109
|
+
padding: 4rem;
|
|
110
|
+
max-width: 1100px;
|
|
111
|
+
margin: 0 auto;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
header {
|
|
115
|
+
margin-bottom: 4rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
h1 {
|
|
119
|
+
font-size: 3rem;
|
|
120
|
+
font-weight: 700;
|
|
121
|
+
margin-bottom: 1rem;
|
|
122
|
+
letter-spacing: -0.02em;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.description {
|
|
126
|
+
font-size: 1.1rem;
|
|
127
|
+
color: var(--text-muted);
|
|
128
|
+
line-height: 1.7;
|
|
129
|
+
max-width: 800px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.endpoint-section {
|
|
133
|
+
margin-bottom: 4rem;
|
|
134
|
+
scroll-margin-top: 4rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.endpoint-header {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 1.5rem;
|
|
141
|
+
margin-bottom: 1.5rem;
|
|
142
|
+
padding-bottom: 1rem;
|
|
143
|
+
border-bottom: 1px solid var(--border);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.method-badge {
|
|
147
|
+
padding: 0.4rem 0.8rem;
|
|
148
|
+
border-radius: 6px;
|
|
149
|
+
font-size: 0.8rem;
|
|
150
|
+
font-weight: 700;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
min-width: 70px;
|
|
153
|
+
text-align: center;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.method-get {
|
|
157
|
+
background: rgba(16, 185, 129, 0.1);
|
|
158
|
+
color: var(--get);
|
|
159
|
+
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
160
|
+
}
|
|
161
|
+
.method-post {
|
|
162
|
+
background: rgba(59, 130, 246, 0.1);
|
|
163
|
+
color: var(--post);
|
|
164
|
+
border: 1px solid rgba(59, 130, 246, 0.2);
|
|
165
|
+
}
|
|
166
|
+
.method-put {
|
|
167
|
+
background: rgba(245, 158, 11, 0.1);
|
|
168
|
+
color: var(--put);
|
|
169
|
+
border: 1px solid rgba(245, 158, 11, 0.2);
|
|
170
|
+
}
|
|
171
|
+
.method-delete {
|
|
172
|
+
background: rgba(239, 68, 68, 0.1);
|
|
173
|
+
color: var(--delete);
|
|
174
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.path {
|
|
178
|
+
font-family: "Monaco", "Consolas", monospace;
|
|
179
|
+
font-size: 1.1rem;
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
color: var(--text);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.endpoint-card {
|
|
185
|
+
background: var(--surface);
|
|
186
|
+
border: 1px solid var(--border);
|
|
187
|
+
border-radius: 16px;
|
|
188
|
+
padding: 2rem;
|
|
189
|
+
margin-top: 1rem;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.summary {
|
|
193
|
+
font-size: 1.25rem;
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
margin-bottom: 1rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.detail-item {
|
|
199
|
+
margin-top: 2rem;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.detail-label {
|
|
203
|
+
font-size: 0.8rem;
|
|
204
|
+
font-weight: 700;
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
color: var(--text-muted);
|
|
207
|
+
margin-bottom: 0.75rem;
|
|
208
|
+
display: block;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.loader {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
align-items: center;
|
|
215
|
+
justify-content: center;
|
|
216
|
+
gap: 1.5rem;
|
|
217
|
+
height: 60vh;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.spinner {
|
|
221
|
+
width: 40px;
|
|
222
|
+
height: 40px;
|
|
223
|
+
border: 3px solid rgba(99, 102, 241, 0.1);
|
|
224
|
+
border-top-color: var(--primary);
|
|
225
|
+
border-radius: 50%;
|
|
226
|
+
animation: spin 0.8s linear infinite;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@keyframes spin {
|
|
230
|
+
to {
|
|
231
|
+
transform: rotate(360deg);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#error-view {
|
|
236
|
+
display: none;
|
|
237
|
+
background: rgba(239, 68, 68, 0.05);
|
|
238
|
+
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
239
|
+
padding: 2rem;
|
|
240
|
+
border-radius: 12px;
|
|
241
|
+
color: #ef4444;
|
|
242
|
+
}
|
|
243
|
+
</style>
|
|
244
|
+
</head>
|
|
245
|
+
<body>
|
|
246
|
+
<aside>
|
|
247
|
+
<div class="logo">
|
|
248
|
+
<div class="logo-box"></div>
|
|
249
|
+
XyPriss Docs
|
|
250
|
+
</div>
|
|
251
|
+
<div class="nav-group-title">Endpoints</div>
|
|
252
|
+
<nav id="sidebar-nav">
|
|
253
|
+
<!-- Sidebar links generated dynamically -->
|
|
254
|
+
</nav>
|
|
255
|
+
</aside>
|
|
256
|
+
|
|
257
|
+
<main>
|
|
258
|
+
<div id="loading-view" class="loader">
|
|
259
|
+
<div class="spinner"></div>
|
|
260
|
+
<p style="color: var(--text-muted)">
|
|
261
|
+
Synchronizing with API Core...
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div id="error-view">
|
|
266
|
+
<h3>Failed to load API specification</h3>
|
|
267
|
+
<p id="error-msg"></p>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div id="docs-view" style="display: none">
|
|
271
|
+
<header>
|
|
272
|
+
<h1 id="doc-title">{{title}}</h1>
|
|
273
|
+
<p class="description" id="doc-description"></p>
|
|
274
|
+
</header>
|
|
275
|
+
|
|
276
|
+
<div id="endpoints-container">
|
|
277
|
+
<!-- Endpoints generated dynamically -->
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</main>
|
|
281
|
+
|
|
282
|
+
<script>
|
|
283
|
+
const specUrl = "{{specUrl}}";
|
|
284
|
+
const endpointsContainer = document.getElementById(
|
|
285
|
+
"endpoints-container",
|
|
286
|
+
);
|
|
287
|
+
const sidebarNav = document.getElementById("sidebar-nav");
|
|
288
|
+
const loadingView = document.getElementById("loading-view");
|
|
289
|
+
const docsView = document.getElementById("docs-view");
|
|
290
|
+
const errorView = document.getElementById("error-view");
|
|
291
|
+
|
|
292
|
+
async function init() {
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(specUrl);
|
|
295
|
+
if (!response.ok)
|
|
296
|
+
throw new Error(`HTTP Error: ${response.status}`);
|
|
297
|
+
const spec = await response.json();
|
|
298
|
+
render(spec);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
loadingView.style.display = "none";
|
|
301
|
+
errorView.style.display = "block";
|
|
302
|
+
document.getElementById("error-msg").innerText =
|
|
303
|
+
err.message;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function render(spec) {
|
|
308
|
+
document.getElementById("doc-title").innerText =
|
|
309
|
+
spec.info.title || "API Documentation";
|
|
310
|
+
document.getElementById("doc-description").innerText =
|
|
311
|
+
spec.info.description || "";
|
|
312
|
+
|
|
313
|
+
const paths = spec.paths || {};
|
|
314
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
315
|
+
for (const [method, details] of Object.entries(methods)) {
|
|
316
|
+
const id = `${method}-${path.replace(/\//g, "-")}`;
|
|
317
|
+
|
|
318
|
+
// Create Sidebar Link
|
|
319
|
+
const navItem = document.createElement("a");
|
|
320
|
+
navItem.href = `#${id}`;
|
|
321
|
+
navItem.className = "nav-item";
|
|
322
|
+
navItem.innerText = `${method.toUpperCase()} ${path}`;
|
|
323
|
+
sidebarNav.appendChild(navItem);
|
|
324
|
+
|
|
325
|
+
// Create Endpoint Section
|
|
326
|
+
const section = document.createElement("section");
|
|
327
|
+
section.className = "endpoint-section";
|
|
328
|
+
section.id = id;
|
|
329
|
+
|
|
330
|
+
section.innerHTML = `
|
|
331
|
+
<div class="endpoint-header">
|
|
332
|
+
<span class="method-badge method-${method}">${method}</span>
|
|
333
|
+
<span class="path">${path}</span>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="endpoint-card">
|
|
336
|
+
<div class="summary">${details.summary || "No Summary"}</div>
|
|
337
|
+
<p style="color: var(--text-muted)">${details.description || "No description provided."}</p>
|
|
338
|
+
|
|
339
|
+
${
|
|
340
|
+
details.parameters && details.parameters.length
|
|
341
|
+
? `
|
|
342
|
+
<div class="detail-item">
|
|
343
|
+
<span class="detail-label">Parameters</span>
|
|
344
|
+
<ul style="list-style: none; color: var(--text-muted)">
|
|
345
|
+
${details.parameters.map((p) => `<li><code>${p.name}</code> (${p.in})</li>`).join("")}
|
|
346
|
+
</ul>
|
|
347
|
+
</div>
|
|
348
|
+
`
|
|
349
|
+
: ""
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
<div class="detail-item">
|
|
353
|
+
<span class="detail-label">Responses</span>
|
|
354
|
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
355
|
+
${Object.entries(details.responses)
|
|
356
|
+
.map(
|
|
357
|
+
([code, res]) => `
|
|
358
|
+
<div style="background: rgba(255,255,255,0.03); padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--border)">
|
|
359
|
+
<span style="font-weight: 700; color: ${code.startsWith("2") ? "var(--get)" : "var(--delete)"}">${code}</span>
|
|
360
|
+
<span style="font-size: 0.8rem; margin-left: 0.5rem">${res.description}</span>
|
|
361
|
+
</div>
|
|
362
|
+
`,
|
|
363
|
+
)
|
|
364
|
+
.join("")}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
`;
|
|
369
|
+
endpointsContainer.appendChild(section);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
loadingView.style.display = "none";
|
|
374
|
+
docsView.style.display = "block";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
init();
|
|
378
|
+
</script>
|
|
379
|
+
</body>
|
|
380
|
+
</html>
|
|
381
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface SwaggerConfig {
|
|
2
|
+
/**
|
|
3
|
+
* The path where the UI will be served.
|
|
4
|
+
* @default "/docs"
|
|
5
|
+
*/
|
|
6
|
+
path?: string;
|
|
7
|
+
|
|
8
|
+
title?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Port to launch the isolated documentation server on.
|
|
13
|
+
* @default 7070
|
|
14
|
+
*/
|
|
15
|
+
port?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export interface ISwaggerJSONStructure {
|
|
21
|
+
name: string;
|
|
22
|
+
version: string;
|
|
23
|
+
description: string;
|
|
24
|
+
|
|
25
|
+
}
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function getSwaggerUIHtml(specUrl: string, title: string): string {
|
|
2
|
+
const templatePath = require("path").join(
|
|
3
|
+
__dirname,
|
|
4
|
+
"..",
|
|
5
|
+
"src",
|
|
6
|
+
"template",
|
|
7
|
+
"ui.html",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
let html = require("fs").readFileSync(templatePath, "utf-8");
|
|
12
|
+
|
|
13
|
+
// Simple template engine using {{}} syntax
|
|
14
|
+
html = html.replace(/\{\{title\}\}/g, title);
|
|
15
|
+
html = html.replace(/\{\{specUrl\}\}/g, specUrl);
|
|
16
|
+
|
|
17
|
+
return html;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error(
|
|
20
|
+
`[SWAGGER] Error reading UI template at ${templatePath}:`,
|
|
21
|
+
error,
|
|
22
|
+
);
|
|
23
|
+
return `<h1>Error loading Swagger UI</h1><p>${String(error)}</p>`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
// "rootDir": "./src",
|
|
8
|
+
"rootDirs": ["./src", "../../src"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"xypriss": ["../../src/index.ts"] // for local dev purpose
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*", "../../src/index.ts"]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|