hasancode-api-docs 1.0.8 → 1.0.10
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/client/dist/assets/{index-CAGuZ--I.js → index-ztS3byLi.js} +1 -1
- package/client/dist/assets/vendor-DILJPn5m.js +16 -0
- package/client/dist/index.html +4 -4
- package/dist/middleware/ApiDoc.d.ts +37 -0
- package/dist/middleware/ApiDoc.js +176 -141
- package/package.json +1 -1
- package/client/dist/assets/vendor-r6CGHiWc.js +0 -16
package/client/dist/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="./logo.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
/>
|
|
14
14
|
|
|
15
15
|
<title>API Doc</title>
|
|
16
|
-
<script type="module" crossorigin src="
|
|
17
|
-
<link rel="modulepreload" crossorigin href="
|
|
18
|
-
<link rel="stylesheet" crossorigin href="
|
|
16
|
+
<script type="module" crossorigin src="./assets/index-ztS3byLi.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="./assets/vendor-DILJPn5m.js">
|
|
18
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BddYm5rR.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
|
21
21
|
<div id="root"></div>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import { RouteMetadata } from "../registry/RouteRegistry";
|
|
3
3
|
import { CodeThemeValues } from "../code-theme";
|
|
4
|
+
/**
|
|
5
|
+
* API Documentation configuration
|
|
6
|
+
*/
|
|
4
7
|
export interface ApiDocConfig {
|
|
5
8
|
title: string;
|
|
6
9
|
version: string;
|
|
@@ -19,20 +22,54 @@ export interface ApiDocConfig {
|
|
|
19
22
|
bearerFormat?: string;
|
|
20
23
|
};
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Main API Documentation class
|
|
27
|
+
*/
|
|
22
28
|
export declare class ApiDoc {
|
|
23
29
|
private config;
|
|
24
30
|
private clientPath;
|
|
25
31
|
private readonly DOCS_PATH;
|
|
26
32
|
private configReady;
|
|
27
33
|
constructor(config: ApiDocConfig);
|
|
34
|
+
/**
|
|
35
|
+
* Normalize configuration with defaults
|
|
36
|
+
*/
|
|
28
37
|
private normalizeConfig;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve client path
|
|
40
|
+
*/
|
|
29
41
|
private resolveClientPath;
|
|
42
|
+
/**
|
|
43
|
+
* Convert route metadata to API documentation format
|
|
44
|
+
*/
|
|
30
45
|
private convertRoutesToSections;
|
|
46
|
+
/**
|
|
47
|
+
* Convert single route to section format
|
|
48
|
+
*/
|
|
31
49
|
private convertRouteToSection;
|
|
50
|
+
/**
|
|
51
|
+
* Get Express middleware - automatically mounts at /api-docs
|
|
52
|
+
* Usage: app.use(apiDoc.middleware())
|
|
53
|
+
*/
|
|
32
54
|
middleware(): Router;
|
|
55
|
+
/**
|
|
56
|
+
* Serve configuration
|
|
57
|
+
*/
|
|
33
58
|
private serveConfig;
|
|
59
|
+
/**
|
|
60
|
+
* Serve the React app
|
|
61
|
+
*/
|
|
34
62
|
private serveApp;
|
|
63
|
+
/**
|
|
64
|
+
* Get all registered routes
|
|
65
|
+
*/
|
|
35
66
|
getRoutes(): RouteMetadata[];
|
|
67
|
+
/**
|
|
68
|
+
* Clear all routes (useful for testing)
|
|
69
|
+
*/
|
|
36
70
|
clearRoutes(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Get the documentation path
|
|
73
|
+
*/
|
|
37
74
|
getDocsPath(): string;
|
|
38
75
|
}
|
|
@@ -49,28 +49,37 @@ const fs = __importStar(require("fs"));
|
|
|
49
49
|
const marked_1 = require("marked");
|
|
50
50
|
const RouteRegistry_1 = require("../registry/RouteRegistry");
|
|
51
51
|
const code_theme_1 = require("../code-theme");
|
|
52
|
+
/**
|
|
53
|
+
* Main API Documentation class
|
|
54
|
+
*/
|
|
52
55
|
class ApiDoc {
|
|
53
56
|
constructor(config) {
|
|
54
|
-
this.DOCS_PATH = "/api-docs";
|
|
57
|
+
this.DOCS_PATH = "/api-docs"; // Fixed path
|
|
55
58
|
this.clientPath = this.resolveClientPath();
|
|
56
|
-
this.config = config;
|
|
59
|
+
this.config = config; // Set initial config
|
|
57
60
|
this.configReady = this.normalizeConfig(config);
|
|
58
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Normalize configuration with defaults
|
|
64
|
+
*/
|
|
59
65
|
normalizeConfig(config) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.config = Object.assign(Object.assign({}, config), { baseUrl: config.baseUrl || "http://localhost:5000", theme: Object.assign({ primaryColor: "#3b82f6", secondaryColor: "#8b5cf6", accentColor: "#10b981", backgroundColor: "#ffffff", sidebarBackgroundColor: "#1f2937", codeTheme: code_theme_1.CODE_THEMES.VITESSE_BLACK }, config.theme) });
|
|
67
|
-
});
|
|
66
|
+
// Convert description to HTML if it exists
|
|
67
|
+
if (config === null || config === void 0 ? void 0 : config.description) {
|
|
68
|
+
const html = marked_1.marked.parse(config.description.toString());
|
|
69
|
+
config.description = '<div class="md__code">' + html + "</div>";
|
|
70
|
+
}
|
|
71
|
+
this.config = Object.assign(Object.assign({}, config), { baseUrl: config.baseUrl || "http://localhost:5000", theme: Object.assign({ primaryColor: "#3b82f6", secondaryColor: "#8b5cf6", accentColor: "#10b981", backgroundColor: "#ffffff", sidebarBackgroundColor: "#1f2937", codeTheme: code_theme_1.CODE_THEMES.VITESSE_BLACK }, config.theme) });
|
|
68
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve client path
|
|
75
|
+
*/
|
|
69
76
|
resolveClientPath() {
|
|
70
77
|
const possiblePaths = [
|
|
71
78
|
path.join(__dirname, "../../client/dist"),
|
|
72
79
|
path.join(__dirname, "../client/dist"),
|
|
73
80
|
path.join(__dirname, "../../../client/dist"),
|
|
81
|
+
// Fallback for some npm structures
|
|
82
|
+
path.join(process.cwd(), "node_modules/hasancode-api-docs/client/dist"),
|
|
74
83
|
];
|
|
75
84
|
for (const testPath of possiblePaths) {
|
|
76
85
|
if (fs.existsSync(testPath) &&
|
|
@@ -80,118 +89,123 @@ class ApiDoc {
|
|
|
80
89
|
}
|
|
81
90
|
return path.join(__dirname, "../../client/dist");
|
|
82
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Convert route metadata to API documentation format
|
|
94
|
+
*/
|
|
83
95
|
convertRoutesToSections() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
grouped.get(tag).push(route);
|
|
96
|
+
const routes = RouteRegistry_1.routeRegistry.getAll();
|
|
97
|
+
const sections = [];
|
|
98
|
+
// Group routes by tags
|
|
99
|
+
const grouped = new Map();
|
|
100
|
+
const untagged = [];
|
|
101
|
+
for (const route of routes) {
|
|
102
|
+
if (route.tags && route.tags.length > 0) {
|
|
103
|
+
for (const tag of route.tags) {
|
|
104
|
+
if (!grouped.has(tag)) {
|
|
105
|
+
grouped.set(tag, []);
|
|
95
106
|
}
|
|
107
|
+
grouped.get(tag).push(route);
|
|
96
108
|
}
|
|
97
|
-
else {
|
|
98
|
-
untagged.push(route);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
for (const [tag, tagRoutes] of grouped.entries()) {
|
|
102
|
-
const items = yield Promise.all(tagRoutes.map((route) => this.convertRouteToSection(route)));
|
|
103
|
-
sections.push({
|
|
104
|
-
name: tag.toLowerCase().replace(/\s+/g, "-"),
|
|
105
|
-
label: tag,
|
|
106
|
-
type: "GROUP",
|
|
107
|
-
items,
|
|
108
|
-
});
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
sections.push({
|
|
113
|
-
name: "other",
|
|
114
|
-
label: "Other Endpoints",
|
|
115
|
-
type: "GROUP",
|
|
116
|
-
items,
|
|
117
|
-
});
|
|
110
|
+
else {
|
|
111
|
+
untagged.push(route);
|
|
118
112
|
}
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
}
|
|
114
|
+
// Create sections for each tag
|
|
115
|
+
for (const [tag, tagRoutes] of grouped.entries()) {
|
|
116
|
+
sections.push({
|
|
117
|
+
name: tag.toLowerCase().replace(/\s+/g, "-"),
|
|
118
|
+
label: tag,
|
|
119
|
+
type: "GROUP",
|
|
120
|
+
items: tagRoutes.map((route) => this.convertRouteToSection(route)),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Add untagged routes
|
|
124
|
+
if (untagged.length > 0) {
|
|
125
|
+
sections.push({
|
|
126
|
+
name: "other",
|
|
127
|
+
label: "Other Endpoints",
|
|
128
|
+
type: "GROUP",
|
|
129
|
+
items: untagged.map((route) => this.convertRouteToSection(route)),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return sections;
|
|
121
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Convert single route to section format
|
|
136
|
+
*/
|
|
122
137
|
convertRouteToSection(route) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
if (route === null || route === void 0 ? void 0 : route.description) {
|
|
139
|
+
const parsed = marked_1.marked.parse(route.description.toString());
|
|
140
|
+
route.description = '<div class="md__code">' + parsed + "</div>";
|
|
141
|
+
}
|
|
142
|
+
if (route === null || route === void 0 ? void 0 : route.summary) {
|
|
143
|
+
const parsed = marked_1.marked.parse(route.summary.toString());
|
|
144
|
+
route.summary = '<div class="md__code">' + parsed + "</div>";
|
|
145
|
+
}
|
|
146
|
+
if (route === null || route === void 0 ? void 0 : route.note) {
|
|
147
|
+
const parsed = marked_1.marked.parse(route.note.toString());
|
|
148
|
+
route.note = '<div class="md__code">' + parsed + "</div>";
|
|
149
|
+
}
|
|
150
|
+
const section = {
|
|
151
|
+
name: `${route.method}${route.path}`
|
|
152
|
+
.replace(/[/:]/g, "-")
|
|
153
|
+
.replace("--", "-"),
|
|
154
|
+
label: `${route.method.toUpperCase()} ${route.path}`,
|
|
155
|
+
type: "ITEM",
|
|
156
|
+
content: route.description,
|
|
157
|
+
api: {
|
|
158
|
+
method: route.method,
|
|
159
|
+
path: route.path,
|
|
160
|
+
summary: route.summary,
|
|
161
|
+
description: route.description,
|
|
162
|
+
deprecated: route.deprecated,
|
|
163
|
+
note: route.note,
|
|
164
|
+
tags: route.tags,
|
|
165
|
+
auth: route.auth,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
if (route.params && route.params.length > 0) {
|
|
169
|
+
section.api.params = route.params;
|
|
170
|
+
}
|
|
171
|
+
if (route.query && route.query.length > 0) {
|
|
172
|
+
section.api.query = route.query;
|
|
173
|
+
}
|
|
174
|
+
if (route.headers && route.headers.length > 0) {
|
|
175
|
+
section.api.headers = route.headers;
|
|
176
|
+
}
|
|
177
|
+
if (route.body) {
|
|
178
|
+
section.api.body = {
|
|
179
|
+
contentType: route.body.contentType,
|
|
180
|
+
required: route.body.required,
|
|
181
|
+
description: route.body.description,
|
|
182
|
+
schema: route.body.schema,
|
|
183
|
+
example: route.body.example,
|
|
154
184
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
required: route.body.required,
|
|
165
|
-
description: route.body.description,
|
|
166
|
-
schema: route.body.schema,
|
|
167
|
-
example: route.body.example,
|
|
185
|
+
}
|
|
186
|
+
if (route.responses && route.responses.length > 0) {
|
|
187
|
+
section.api.responses = {};
|
|
188
|
+
for (const response of route.responses) {
|
|
189
|
+
section.api.responses[response.statusCode] = {
|
|
190
|
+
description: response.description,
|
|
191
|
+
contentType: response.contentType,
|
|
192
|
+
schema: response.schema,
|
|
193
|
+
example: response.example,
|
|
168
194
|
};
|
|
169
195
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const response of route.responses) {
|
|
173
|
-
section.api.responses[response.statusCode] = {
|
|
174
|
-
description: response.description,
|
|
175
|
-
contentType: response.contentType,
|
|
176
|
-
schema: response.schema,
|
|
177
|
-
example: response.example,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return section;
|
|
182
|
-
});
|
|
196
|
+
}
|
|
197
|
+
return section;
|
|
183
198
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Get Express middleware - automatically mounts at /api-docs
|
|
201
|
+
* Usage: app.use(apiDoc.middleware())
|
|
202
|
+
*/
|
|
187
203
|
middleware() {
|
|
188
204
|
const router = (0, express_1.Router)();
|
|
189
|
-
//
|
|
205
|
+
// 1. Serve config.json
|
|
190
206
|
router.get(`${this.DOCS_PATH}/config.json`, this.serveConfig.bind(this));
|
|
191
|
-
//
|
|
192
|
-
// CRITICAL: Use absolute path match to prevent wildcard route from catching it
|
|
207
|
+
// 2. Serve static assets with explicit MIME types
|
|
193
208
|
router.use(`${this.DOCS_PATH}/assets`, (req, res, next) => {
|
|
194
|
-
// Manually set correct MIME types before expressStatic processes
|
|
195
209
|
const ext = path.extname(req.path).toLowerCase();
|
|
196
210
|
if (ext === ".js") {
|
|
197
211
|
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
@@ -199,36 +213,44 @@ class ApiDoc {
|
|
|
199
213
|
else if (ext === ".css") {
|
|
200
214
|
res.setHeader("Content-Type", "text/css; charset=utf-8");
|
|
201
215
|
}
|
|
202
|
-
else if (ext === ".map") {
|
|
203
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
204
|
-
}
|
|
205
216
|
next();
|
|
206
217
|
}, (0, express_1.static)(path.join(this.clientPath, "assets"), {
|
|
207
218
|
maxAge: "1d",
|
|
208
219
|
etag: true,
|
|
209
|
-
index: false,
|
|
210
|
-
fallthrough: false,
|
|
220
|
+
index: false,
|
|
221
|
+
fallthrough: false,
|
|
211
222
|
}));
|
|
212
|
-
//
|
|
213
|
-
router.
|
|
223
|
+
// 3. Serve the React app
|
|
224
|
+
router.use(this.serveApp.bind(this));
|
|
214
225
|
return router;
|
|
215
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Serve configuration
|
|
229
|
+
*/
|
|
216
230
|
serveConfig(req, res) {
|
|
217
231
|
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
// Wait for config to be ready
|
|
218
233
|
yield this.configReady;
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
// Auto-generate sections from decorated routes
|
|
235
|
+
const sections = this.convertRoutesToSections();
|
|
236
|
+
const runtimeConfig = {
|
|
221
237
|
config: Object.assign(Object.assign({}, this.config), { sections }),
|
|
222
238
|
basePath: this.DOCS_PATH,
|
|
223
|
-
}
|
|
239
|
+
};
|
|
240
|
+
res.json(runtimeConfig);
|
|
224
241
|
});
|
|
225
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Serve the React app
|
|
245
|
+
*/
|
|
226
246
|
serveApp(req, res) {
|
|
227
247
|
const indexPath = path.join(this.clientPath, "index.html");
|
|
228
248
|
if (fs.existsSync(indexPath)) {
|
|
229
249
|
let html = fs.readFileSync(indexPath, "utf-8");
|
|
230
|
-
|
|
231
|
-
|
|
250
|
+
// Use relative base tag so it works when mounted at any prefix
|
|
251
|
+
const baseTag = `<base href="./">`;
|
|
252
|
+
// Insert or replace base tag
|
|
253
|
+
if (html.includes("<base ")) {
|
|
232
254
|
html = html.replace(/<base[^>]*>/i, baseTag);
|
|
233
255
|
}
|
|
234
256
|
else {
|
|
@@ -240,36 +262,49 @@ class ApiDoc {
|
|
|
240
262
|
}
|
|
241
263
|
else {
|
|
242
264
|
res.status(404).send(`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
+
<html>
|
|
266
|
+
<head>
|
|
267
|
+
<title>API Docs - Not Built</title>
|
|
268
|
+
<style>
|
|
269
|
+
body {
|
|
270
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
271
|
+
max-width: 600px;
|
|
272
|
+
margin: 100px auto;
|
|
273
|
+
padding: 20px;
|
|
274
|
+
text-align: center;
|
|
275
|
+
}
|
|
276
|
+
h1 { color: #e53e3e; }
|
|
277
|
+
code {
|
|
278
|
+
background: #f7fafc;
|
|
279
|
+
padding: 2px 6px;
|
|
280
|
+
border-radius: 4px;
|
|
281
|
+
}
|
|
282
|
+
</style>
|
|
283
|
+
</head>
|
|
284
|
+
<body>
|
|
285
|
+
<h1>⚠️ Client Not Built</h1>
|
|
286
|
+
<p>Please run <code>npm run build</code> first.</p>
|
|
287
|
+
<p><small>Looking at: ${this.clientPath}</small></p>
|
|
288
|
+
</body>
|
|
289
|
+
</html>
|
|
290
|
+
`);
|
|
265
291
|
}
|
|
266
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Get all registered routes
|
|
295
|
+
*/
|
|
267
296
|
getRoutes() {
|
|
268
297
|
return RouteRegistry_1.routeRegistry.getAll();
|
|
269
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Clear all routes (useful for testing)
|
|
301
|
+
*/
|
|
270
302
|
clearRoutes() {
|
|
271
303
|
RouteRegistry_1.routeRegistry.clear();
|
|
272
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Get the documentation path
|
|
307
|
+
*/
|
|
273
308
|
getDocsPath() {
|
|
274
309
|
return this.DOCS_PATH;
|
|
275
310
|
}
|