hasancode-api-docs 1.0.9 → 1.0.11
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 +183 -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,128 @@ 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. Handle trailing slash redirect for the root path
|
|
206
|
+
// This is crucial for relative asset resolution
|
|
207
|
+
router.get(this.DOCS_PATH, (req, res) => {
|
|
208
|
+
res.redirect(301, req.originalUrl + "/");
|
|
209
|
+
});
|
|
210
|
+
// 2. Serve config.json
|
|
190
211
|
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
|
|
212
|
+
// 3. Serve static assets with explicit MIME types
|
|
193
213
|
router.use(`${this.DOCS_PATH}/assets`, (req, res, next) => {
|
|
194
|
-
// Manually set correct MIME types before expressStatic processes
|
|
195
214
|
const ext = path.extname(req.path).toLowerCase();
|
|
196
215
|
if (ext === ".js") {
|
|
197
216
|
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
@@ -199,78 +218,101 @@ class ApiDoc {
|
|
|
199
218
|
else if (ext === ".css") {
|
|
200
219
|
res.setHeader("Content-Type", "text/css; charset=utf-8");
|
|
201
220
|
}
|
|
202
|
-
else if (ext === ".map") {
|
|
203
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
204
|
-
}
|
|
205
221
|
next();
|
|
206
222
|
}, (0, express_1.static)(path.join(this.clientPath, "assets"), {
|
|
207
223
|
maxAge: "1d",
|
|
208
224
|
etag: true,
|
|
209
|
-
index: false,
|
|
210
|
-
fallthrough: false,
|
|
225
|
+
index: false,
|
|
226
|
+
fallthrough: false,
|
|
211
227
|
}));
|
|
212
|
-
//
|
|
213
|
-
// router.get(`${this.DOCS_PATH}*`, this.serveApp.bind(this));
|
|
228
|
+
// 4. Serve the React app
|
|
214
229
|
router.use(this.serveApp.bind(this));
|
|
215
230
|
return router;
|
|
216
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Serve configuration
|
|
234
|
+
*/
|
|
217
235
|
serveConfig(req, res) {
|
|
218
236
|
return __awaiter(this, void 0, void 0, function* () {
|
|
237
|
+
// Wait for config to be ready
|
|
219
238
|
yield this.configReady;
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
// Auto-generate sections from decorated routes
|
|
240
|
+
const sections = this.convertRoutesToSections();
|
|
241
|
+
const runtimeConfig = {
|
|
222
242
|
config: Object.assign(Object.assign({}, this.config), { sections }),
|
|
223
243
|
basePath: this.DOCS_PATH,
|
|
224
|
-
}
|
|
244
|
+
};
|
|
245
|
+
res.json(runtimeConfig);
|
|
225
246
|
});
|
|
226
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Serve the React app
|
|
250
|
+
*/
|
|
227
251
|
serveApp(req, res) {
|
|
228
252
|
const indexPath = path.join(this.clientPath, "index.html");
|
|
229
253
|
if (fs.existsSync(indexPath)) {
|
|
230
254
|
let html = fs.readFileSync(indexPath, "utf-8");
|
|
231
|
-
|
|
232
|
-
|
|
255
|
+
// Inject base tag with trailing slash for proper asset resolution
|
|
256
|
+
// We use relative './' which depends on the URL ending with a slash
|
|
257
|
+
const baseTag = `<base href="./">`;
|
|
258
|
+
// Insert or replace base tag
|
|
259
|
+
if (html.includes("<base ")) {
|
|
233
260
|
html = html.replace(/<base[^>]*>/i, baseTag);
|
|
234
261
|
}
|
|
235
262
|
else {
|
|
236
263
|
html = html.replace(/<head>/i, `<head>${baseTag}`);
|
|
237
264
|
}
|
|
265
|
+
// Also ensure any absolute references in index.html are corrected if they slipped through
|
|
266
|
+
html = html.replace(/\/api-docs\/assets\//g, "assets/");
|
|
238
267
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
239
268
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
240
269
|
res.send(html);
|
|
241
270
|
}
|
|
242
271
|
else {
|
|
243
272
|
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
|
-
|
|
273
|
+
<html>
|
|
274
|
+
<head>
|
|
275
|
+
<title>API Docs - Not Built</title>
|
|
276
|
+
<style>
|
|
277
|
+
body {
|
|
278
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
279
|
+
max-width: 600px;
|
|
280
|
+
margin: 100px auto;
|
|
281
|
+
padding: 20px;
|
|
282
|
+
text-align: center;
|
|
283
|
+
}
|
|
284
|
+
h1 { color: #e53e3e; }
|
|
285
|
+
code {
|
|
286
|
+
background: #f7fafc;
|
|
287
|
+
padding: 2px 6px;
|
|
288
|
+
border-radius: 4px;
|
|
289
|
+
}
|
|
290
|
+
</style>
|
|
291
|
+
</head>
|
|
292
|
+
<body>
|
|
293
|
+
<h1>⚠️ Client Not Built</h1>
|
|
294
|
+
<p>Please run <code>npm run build</code> first.</p>
|
|
295
|
+
<p><small>Looking at: ${this.clientPath}</small></p>
|
|
296
|
+
</body>
|
|
297
|
+
</html>
|
|
298
|
+
`);
|
|
266
299
|
}
|
|
267
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Get all registered routes
|
|
303
|
+
*/
|
|
268
304
|
getRoutes() {
|
|
269
305
|
return RouteRegistry_1.routeRegistry.getAll();
|
|
270
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Clear all routes (useful for testing)
|
|
309
|
+
*/
|
|
271
310
|
clearRoutes() {
|
|
272
311
|
RouteRegistry_1.routeRegistry.clear();
|
|
273
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Get the documentation path
|
|
315
|
+
*/
|
|
274
316
|
getDocsPath() {
|
|
275
317
|
return this.DOCS_PATH;
|
|
276
318
|
}
|