hasancode-api-docs 1.0.7 → 1.0.9
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/dist/middleware/ApiDoc.d.ts +0 -37
- package/dist/middleware/ApiDoc.js +150 -179
- package/package.json +1 -1
|
@@ -1,9 +1,6 @@
|
|
|
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
|
-
*/
|
|
7
4
|
export interface ApiDocConfig {
|
|
8
5
|
title: string;
|
|
9
6
|
version: string;
|
|
@@ -22,54 +19,20 @@ export interface ApiDocConfig {
|
|
|
22
19
|
bearerFormat?: string;
|
|
23
20
|
};
|
|
24
21
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Main API Documentation class
|
|
27
|
-
*/
|
|
28
22
|
export declare class ApiDoc {
|
|
29
23
|
private config;
|
|
30
24
|
private clientPath;
|
|
31
25
|
private readonly DOCS_PATH;
|
|
32
26
|
private configReady;
|
|
33
27
|
constructor(config: ApiDocConfig);
|
|
34
|
-
/**
|
|
35
|
-
* Normalize configuration with defaults
|
|
36
|
-
*/
|
|
37
28
|
private normalizeConfig;
|
|
38
|
-
/**
|
|
39
|
-
* Resolve client path
|
|
40
|
-
*/
|
|
41
29
|
private resolveClientPath;
|
|
42
|
-
/**
|
|
43
|
-
* Convert route metadata to API documentation format
|
|
44
|
-
*/
|
|
45
30
|
private convertRoutesToSections;
|
|
46
|
-
/**
|
|
47
|
-
* Convert single route to section format
|
|
48
|
-
*/
|
|
49
31
|
private convertRouteToSection;
|
|
50
|
-
/**
|
|
51
|
-
* Get Express middleware - automatically mounts at /api-docs
|
|
52
|
-
* Usage: app.use(apiDoc.middleware())
|
|
53
|
-
*/
|
|
54
32
|
middleware(): Router;
|
|
55
|
-
/**
|
|
56
|
-
* Serve configuration
|
|
57
|
-
*/
|
|
58
33
|
private serveConfig;
|
|
59
|
-
/**
|
|
60
|
-
* Serve the React app
|
|
61
|
-
*/
|
|
62
34
|
private serveApp;
|
|
63
|
-
/**
|
|
64
|
-
* Get all registered routes
|
|
65
|
-
*/
|
|
66
35
|
getRoutes(): RouteMetadata[];
|
|
67
|
-
/**
|
|
68
|
-
* Clear all routes (useful for testing)
|
|
69
|
-
*/
|
|
70
36
|
clearRoutes(): void;
|
|
71
|
-
/**
|
|
72
|
-
* Get the documentation path
|
|
73
|
-
*/
|
|
74
37
|
getDocsPath(): string;
|
|
75
38
|
}
|
|
@@ -49,30 +49,23 @@ 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
|
-
*/
|
|
55
52
|
class ApiDoc {
|
|
56
53
|
constructor(config) {
|
|
57
|
-
this.DOCS_PATH = "/api-docs";
|
|
54
|
+
this.DOCS_PATH = "/api-docs";
|
|
58
55
|
this.clientPath = this.resolveClientPath();
|
|
59
|
-
this.config = config;
|
|
56
|
+
this.config = config;
|
|
60
57
|
this.configReady = this.normalizeConfig(config);
|
|
61
58
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Normalize configuration with defaults
|
|
64
|
-
*/
|
|
65
59
|
normalizeConfig(config) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
// Convert description to HTML if it exists
|
|
62
|
+
if (config === null || config === void 0 ? void 0 : config.description) {
|
|
63
|
+
const html = yield marked_1.marked.parse(config.description.toString());
|
|
64
|
+
config.description = '<div class="md__code">' + html + "</div>";
|
|
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
|
+
});
|
|
72
68
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Resolve client path
|
|
75
|
-
*/
|
|
76
69
|
resolveClientPath() {
|
|
77
70
|
const possiblePaths = [
|
|
78
71
|
path.join(__dirname, "../../client/dist"),
|
|
@@ -87,219 +80,197 @@ class ApiDoc {
|
|
|
87
80
|
}
|
|
88
81
|
return path.join(__dirname, "../../client/dist");
|
|
89
82
|
}
|
|
90
|
-
/**
|
|
91
|
-
* Convert route metadata to API documentation format
|
|
92
|
-
*/
|
|
93
83
|
convertRoutesToSections() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
const routes = RouteRegistry_1.routeRegistry.getAll();
|
|
86
|
+
const sections = [];
|
|
87
|
+
const grouped = new Map();
|
|
88
|
+
const untagged = [];
|
|
89
|
+
for (const route of routes) {
|
|
90
|
+
if (route.tags && route.tags.length > 0) {
|
|
91
|
+
for (const tag of route.tags) {
|
|
92
|
+
if (!grouped.has(tag))
|
|
93
|
+
grouped.set(tag, []);
|
|
94
|
+
grouped.get(tag).push(route);
|
|
104
95
|
}
|
|
105
|
-
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
untagged.push(route);
|
|
106
99
|
}
|
|
107
100
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
});
|
|
110
109
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (untagged.length > 0) {
|
|
123
|
-
sections.push({
|
|
124
|
-
name: "other",
|
|
125
|
-
label: "Other Endpoints",
|
|
126
|
-
type: "GROUP",
|
|
127
|
-
items: untagged.map((route) => this.convertRouteToSection(route)),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return sections;
|
|
110
|
+
if (untagged.length > 0) {
|
|
111
|
+
const items = yield Promise.all(untagged.map((route) => this.convertRouteToSection(route)));
|
|
112
|
+
sections.push({
|
|
113
|
+
name: "other",
|
|
114
|
+
label: "Other Endpoints",
|
|
115
|
+
type: "GROUP",
|
|
116
|
+
items,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return sections;
|
|
120
|
+
});
|
|
131
121
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Convert single route to section format
|
|
134
|
-
*/
|
|
135
122
|
convertRouteToSection(route) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
route
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
route
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
route
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
section.api.params = route.params;
|
|
168
|
-
}
|
|
169
|
-
if (route.query && route.query.length > 0) {
|
|
170
|
-
section.api.query = route.query;
|
|
171
|
-
}
|
|
172
|
-
if (route.headers && route.headers.length > 0) {
|
|
173
|
-
section.api.headers = route.headers;
|
|
174
|
-
}
|
|
175
|
-
if (route.body) {
|
|
176
|
-
section.api.body = {
|
|
177
|
-
contentType: route.body.contentType,
|
|
178
|
-
required: route.body.required,
|
|
179
|
-
description: route.body.description,
|
|
180
|
-
schema: route.body.schema,
|
|
181
|
-
example: route.body.example,
|
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
var _a, _b, _c, _d;
|
|
125
|
+
if (route === null || route === void 0 ? void 0 : route.description) {
|
|
126
|
+
const parsed = yield marked_1.marked.parse(route.description.toString());
|
|
127
|
+
route.description = '<div class="md__code">' + parsed + "</div>";
|
|
128
|
+
}
|
|
129
|
+
if (route === null || route === void 0 ? void 0 : route.summary) {
|
|
130
|
+
const parsed = yield marked_1.marked.parse(route.summary.toString());
|
|
131
|
+
route.summary = '<div class="md__code">' + parsed + "</div>";
|
|
132
|
+
}
|
|
133
|
+
if (route === null || route === void 0 ? void 0 : route.note) {
|
|
134
|
+
const parsed = yield marked_1.marked.parse(route.note.toString());
|
|
135
|
+
route.note = '<div class="md__code">' + parsed + "</div>";
|
|
136
|
+
}
|
|
137
|
+
const section = {
|
|
138
|
+
name: `${route.method}${route.path}`
|
|
139
|
+
.replace(/[/:]/g, "-")
|
|
140
|
+
.replace("--", "-"),
|
|
141
|
+
label: `${route.method.toUpperCase()} ${route.path}`,
|
|
142
|
+
type: "ITEM",
|
|
143
|
+
content: route.description,
|
|
144
|
+
api: {
|
|
145
|
+
method: route.method,
|
|
146
|
+
path: route.path,
|
|
147
|
+
summary: route.summary,
|
|
148
|
+
description: route.description,
|
|
149
|
+
deprecated: route.deprecated,
|
|
150
|
+
note: route.note,
|
|
151
|
+
tags: route.tags,
|
|
152
|
+
auth: route.auth,
|
|
153
|
+
},
|
|
182
154
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
155
|
+
if ((_a = route.params) === null || _a === void 0 ? void 0 : _a.length)
|
|
156
|
+
section.api.params = route.params;
|
|
157
|
+
if ((_b = route.query) === null || _b === void 0 ? void 0 : _b.length)
|
|
158
|
+
section.api.query = route.query;
|
|
159
|
+
if ((_c = route.headers) === null || _c === void 0 ? void 0 : _c.length)
|
|
160
|
+
section.api.headers = route.headers;
|
|
161
|
+
if (route.body) {
|
|
162
|
+
section.api.body = {
|
|
163
|
+
contentType: route.body.contentType,
|
|
164
|
+
required: route.body.required,
|
|
165
|
+
description: route.body.description,
|
|
166
|
+
schema: route.body.schema,
|
|
167
|
+
example: route.body.example,
|
|
192
168
|
};
|
|
193
169
|
}
|
|
194
|
-
|
|
195
|
-
|
|
170
|
+
if ((_d = route.responses) === null || _d === void 0 ? void 0 : _d.length) {
|
|
171
|
+
section.api.responses = {};
|
|
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
183
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
*/
|
|
184
|
+
// ══════════════════════════════════════════════════════════════
|
|
185
|
+
// FIXED: Static file serving with correct MIME types
|
|
186
|
+
// ══════════════════════════════════════════════════════════════
|
|
201
187
|
middleware() {
|
|
202
188
|
const router = (0, express_1.Router)();
|
|
189
|
+
// ── 1. Serve config.json (MUST come before static assets) ────
|
|
203
190
|
router.get(`${this.DOCS_PATH}/config.json`, this.serveConfig.bind(this));
|
|
204
|
-
|
|
191
|
+
// ── 2. Serve static assets (CSS, JS, etc.) ───────────────────
|
|
192
|
+
// CRITICAL: Use absolute path match to prevent wildcard route from catching it
|
|
193
|
+
router.use(`${this.DOCS_PATH}/assets`, (req, res, next) => {
|
|
194
|
+
// Manually set correct MIME types before expressStatic processes
|
|
195
|
+
const ext = path.extname(req.path).toLowerCase();
|
|
196
|
+
if (ext === ".js") {
|
|
197
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
198
|
+
}
|
|
199
|
+
else if (ext === ".css") {
|
|
200
|
+
res.setHeader("Content-Type", "text/css; charset=utf-8");
|
|
201
|
+
}
|
|
202
|
+
else if (ext === ".map") {
|
|
203
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
204
|
+
}
|
|
205
|
+
next();
|
|
206
|
+
}, (0, express_1.static)(path.join(this.clientPath, "assets"), {
|
|
205
207
|
maxAge: "1d",
|
|
206
208
|
etag: true,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (path.endsWith(".js")) {
|
|
210
|
-
res.setHeader("Content-Type", "application/javascript");
|
|
211
|
-
}
|
|
212
|
-
else if (path.endsWith(".css")) {
|
|
213
|
-
res.setHeader("Content-Type", "text/css");
|
|
214
|
-
}
|
|
215
|
-
},
|
|
209
|
+
index: false, // Don't serve index.html from assets
|
|
210
|
+
fallthrough: false, // Return 404 if file not found
|
|
216
211
|
}));
|
|
212
|
+
// ── 3. Serve React app (catch-all, MUST come last) ───────────
|
|
213
|
+
// router.get(`${this.DOCS_PATH}*`, this.serveApp.bind(this));
|
|
217
214
|
router.use(this.serveApp.bind(this));
|
|
218
215
|
return router;
|
|
219
216
|
}
|
|
220
|
-
/**
|
|
221
|
-
* Serve configuration
|
|
222
|
-
*/
|
|
223
217
|
serveConfig(req, res) {
|
|
224
218
|
return __awaiter(this, void 0, void 0, function* () {
|
|
225
|
-
// Wait for config to be ready
|
|
226
219
|
yield this.configReady;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const runtimeConfig = {
|
|
220
|
+
const sections = yield this.convertRoutesToSections();
|
|
221
|
+
res.json({
|
|
230
222
|
config: Object.assign(Object.assign({}, this.config), { sections }),
|
|
231
223
|
basePath: this.DOCS_PATH,
|
|
232
|
-
};
|
|
233
|
-
res.json(runtimeConfig);
|
|
224
|
+
});
|
|
234
225
|
});
|
|
235
226
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Serve the React app
|
|
238
|
-
*/
|
|
239
227
|
serveApp(req, res) {
|
|
240
228
|
const indexPath = path.join(this.clientPath, "index.html");
|
|
241
229
|
if (fs.existsSync(indexPath)) {
|
|
242
230
|
let html = fs.readFileSync(indexPath, "utf-8");
|
|
243
|
-
// Inject base tag with trailing slash for proper asset resolution
|
|
244
231
|
const baseTag = `<base href="${this.DOCS_PATH}/">`;
|
|
245
|
-
// Insert base tag right after <head> or before any other tags
|
|
246
232
|
if (html.includes("<base")) {
|
|
247
|
-
// Replace existing base tag
|
|
248
233
|
html = html.replace(/<base[^>]*>/i, baseTag);
|
|
249
234
|
}
|
|
250
235
|
else {
|
|
251
|
-
// Insert new base tag
|
|
252
236
|
html = html.replace(/<head>/i, `<head>${baseTag}`);
|
|
253
237
|
}
|
|
254
|
-
res.setHeader("Content-Type", "text/html");
|
|
238
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
255
239
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
256
240
|
res.send(html);
|
|
257
241
|
}
|
|
258
242
|
else {
|
|
259
243
|
res.status(404).send(`
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
<p><small>Looking at: ${this.clientPath}</small></p>
|
|
283
|
-
</body>
|
|
284
|
-
</html>
|
|
285
|
-
`);
|
|
244
|
+
<html>
|
|
245
|
+
<head>
|
|
246
|
+
<title>API Docs - Not Built</title>
|
|
247
|
+
<style>
|
|
248
|
+
body {
|
|
249
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
250
|
+
max-width: 600px;
|
|
251
|
+
margin: 100px auto;
|
|
252
|
+
padding: 20px;
|
|
253
|
+
text-align: center;
|
|
254
|
+
}
|
|
255
|
+
h1 { color: #e53e3e; }
|
|
256
|
+
code { background: #f7fafc; padding: 2px 6px; border-radius: 4px; }
|
|
257
|
+
</style>
|
|
258
|
+
</head>
|
|
259
|
+
<body>
|
|
260
|
+
<h1>⚠️ Client Not Built</h1>
|
|
261
|
+
<p>Run <code>npm run build</code> first.</p>
|
|
262
|
+
<small>Looking at: ${this.clientPath}</small>
|
|
263
|
+
</body>
|
|
264
|
+
</html>
|
|
265
|
+
`);
|
|
286
266
|
}
|
|
287
267
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Get all registered routes
|
|
290
|
-
*/
|
|
291
268
|
getRoutes() {
|
|
292
269
|
return RouteRegistry_1.routeRegistry.getAll();
|
|
293
270
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Clear all routes (useful for testing)
|
|
296
|
-
*/
|
|
297
271
|
clearRoutes() {
|
|
298
272
|
RouteRegistry_1.routeRegistry.clear();
|
|
299
273
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Get the documentation path
|
|
302
|
-
*/
|
|
303
274
|
getDocsPath() {
|
|
304
275
|
return this.DOCS_PATH;
|
|
305
276
|
}
|