hasancode-api-docs 1.0.9 → 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 +175 -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,37 +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.get(`${this.DOCS_PATH}*`, this.serveApp.bind(this));
|
|
223
|
+
// 3. Serve the React app
|
|
214
224
|
router.use(this.serveApp.bind(this));
|
|
215
225
|
return router;
|
|
216
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Serve configuration
|
|
229
|
+
*/
|
|
217
230
|
serveConfig(req, res) {
|
|
218
231
|
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
// Wait for config to be ready
|
|
219
233
|
yield this.configReady;
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
// Auto-generate sections from decorated routes
|
|
235
|
+
const sections = this.convertRoutesToSections();
|
|
236
|
+
const runtimeConfig = {
|
|
222
237
|
config: Object.assign(Object.assign({}, this.config), { sections }),
|
|
223
238
|
basePath: this.DOCS_PATH,
|
|
224
|
-
}
|
|
239
|
+
};
|
|
240
|
+
res.json(runtimeConfig);
|
|
225
241
|
});
|
|
226
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Serve the React app
|
|
245
|
+
*/
|
|
227
246
|
serveApp(req, res) {
|
|
228
247
|
const indexPath = path.join(this.clientPath, "index.html");
|
|
229
248
|
if (fs.existsSync(indexPath)) {
|
|
230
249
|
let html = fs.readFileSync(indexPath, "utf-8");
|
|
231
|
-
|
|
232
|
-
|
|
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 ")) {
|
|
233
254
|
html = html.replace(/<base[^>]*>/i, baseTag);
|
|
234
255
|
}
|
|
235
256
|
else {
|
|
@@ -241,36 +262,49 @@ class ApiDoc {
|
|
|
241
262
|
}
|
|
242
263
|
else {
|
|
243
264
|
res.status(404).send(`
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
`);
|
|
266
291
|
}
|
|
267
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Get all registered routes
|
|
295
|
+
*/
|
|
268
296
|
getRoutes() {
|
|
269
297
|
return RouteRegistry_1.routeRegistry.getAll();
|
|
270
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Clear all routes (useful for testing)
|
|
301
|
+
*/
|
|
271
302
|
clearRoutes() {
|
|
272
303
|
RouteRegistry_1.routeRegistry.clear();
|
|
273
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Get the documentation path
|
|
307
|
+
*/
|
|
274
308
|
getDocsPath() {
|
|
275
309
|
return this.DOCS_PATH;
|
|
276
310
|
}
|