hasancode-api-docs 1.0.7 → 1.0.8

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.
@@ -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"; // Fixed path
54
+ this.DOCS_PATH = "/api-docs";
58
55
  this.clientPath = this.resolveClientPath();
59
- this.config = config; // Set initial 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
- // 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) });
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,196 @@ 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
- const routes = RouteRegistry_1.routeRegistry.getAll();
95
- const sections = [];
96
- // Group routes by tags
97
- const grouped = new Map();
98
- const untagged = [];
99
- for (const route of routes) {
100
- if (route.tags && route.tags.length > 0) {
101
- for (const tag of route.tags) {
102
- if (!grouped.has(tag)) {
103
- grouped.set(tag, []);
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
- grouped.get(tag).push(route);
96
+ }
97
+ else {
98
+ untagged.push(route);
106
99
  }
107
100
  }
108
- else {
109
- untagged.push(route);
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
- // Create sections for each tag
113
- for (const [tag, tagRoutes] of grouped.entries()) {
114
- sections.push({
115
- name: tag.toLowerCase().replace(/\s+/g, "-"),
116
- label: tag,
117
- type: "GROUP",
118
- items: tagRoutes.map((route) => this.convertRouteToSection(route)),
119
- });
120
- }
121
- // Add untagged routes
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
- if (route === null || route === void 0 ? void 0 : route.description) {
137
- const parsed = marked_1.marked.parse(route.description.toString());
138
- route.description = '<div class="md__code">' + parsed + "</div>";
139
- }
140
- if (route === null || route === void 0 ? void 0 : route.summary) {
141
- const parsed = marked_1.marked.parse(route.summary.toString());
142
- route.summary = '<div class="md__code">' + parsed + "</div>";
143
- }
144
- if (route === null || route === void 0 ? void 0 : route.note) {
145
- const parsed = marked_1.marked.parse(route.note.toString());
146
- route.note = '<div class="md__code">' + parsed + "</div>";
147
- }
148
- const section = {
149
- name: `${route.method}${route.path}`
150
- .replace(/[/:]/g, "-")
151
- .replace("--", "-"),
152
- label: `${route.method.toUpperCase()} ${route.path}`,
153
- type: "ITEM",
154
- content: route.description,
155
- api: {
156
- method: route.method,
157
- path: route.path,
158
- summary: route.summary,
159
- description: route.description,
160
- deprecated: route.deprecated,
161
- note: route.note,
162
- tags: route.tags,
163
- auth: route.auth,
164
- },
165
- };
166
- if (route.params && route.params.length > 0) {
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
- if (route.responses && route.responses.length > 0) {
185
- section.api.responses = {};
186
- for (const response of route.responses) {
187
- section.api.responses[response.statusCode] = {
188
- description: response.description,
189
- contentType: response.contentType,
190
- schema: response.schema,
191
- example: response.example,
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
- return section;
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
- * Get Express middleware - automatically mounts at /api-docs
199
- * Usage: app.use(apiDoc.middleware())
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
- router.use(`${this.DOCS_PATH}/assets`, (0, express_1.static)(path.join(this.clientPath, "assets"), {
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
- fallthrough: false,
208
- setHeaders: (res, path) => {
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
  }));
217
- router.use(this.serveApp.bind(this));
212
+ // ── 3. Serve React app (catch-all, MUST come last) ───────────
213
+ router.get(`${this.DOCS_PATH}*`, this.serveApp.bind(this));
218
214
  return router;
219
215
  }
220
- /**
221
- * Serve configuration
222
- */
223
216
  serveConfig(req, res) {
224
217
  return __awaiter(this, void 0, void 0, function* () {
225
- // Wait for config to be ready
226
218
  yield this.configReady;
227
- // Auto-generate sections from decorated routes
228
- const sections = this.convertRoutesToSections();
229
- const runtimeConfig = {
219
+ const sections = yield this.convertRoutesToSections();
220
+ res.json({
230
221
  config: Object.assign(Object.assign({}, this.config), { sections }),
231
222
  basePath: this.DOCS_PATH,
232
- };
233
- res.json(runtimeConfig);
223
+ });
234
224
  });
235
225
  }
236
- /**
237
- * Serve the React app
238
- */
239
226
  serveApp(req, res) {
240
227
  const indexPath = path.join(this.clientPath, "index.html");
241
228
  if (fs.existsSync(indexPath)) {
242
229
  let html = fs.readFileSync(indexPath, "utf-8");
243
- // Inject base tag with trailing slash for proper asset resolution
244
230
  const baseTag = `<base href="${this.DOCS_PATH}/">`;
245
- // Insert base tag right after <head> or before any other tags
246
231
  if (html.includes("<base")) {
247
- // Replace existing base tag
248
232
  html = html.replace(/<base[^>]*>/i, baseTag);
249
233
  }
250
234
  else {
251
- // Insert new base tag
252
235
  html = html.replace(/<head>/i, `<head>${baseTag}`);
253
236
  }
254
- res.setHeader("Content-Type", "text/html");
237
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
255
238
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
256
239
  res.send(html);
257
240
  }
258
241
  else {
259
242
  res.status(404).send(`
260
- <html>
261
- <head>
262
- <title>API Docs - Not Built</title>
263
- <style>
264
- body {
265
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
266
- max-width: 600px;
267
- margin: 100px auto;
268
- padding: 20px;
269
- text-align: center;
270
- }
271
- h1 { color: #e53e3e; }
272
- code {
273
- background: #f7fafc;
274
- padding: 2px 6px;
275
- border-radius: 4px;
276
- }
277
- </style>
278
- </head>
279
- <body>
280
- <h1>⚠️ Client Not Built</h1>
281
- <p>Please run <code>npm run build</code> first.</p>
282
- <p><small>Looking at: ${this.clientPath}</small></p>
283
- </body>
284
- </html>
285
- `);
243
+ <html>
244
+ <head>
245
+ <title>API Docs - Not Built</title>
246
+ <style>
247
+ body {
248
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
249
+ max-width: 600px;
250
+ margin: 100px auto;
251
+ padding: 20px;
252
+ text-align: center;
253
+ }
254
+ h1 { color: #e53e3e; }
255
+ code { background: #f7fafc; padding: 2px 6px; border-radius: 4px; }
256
+ </style>
257
+ </head>
258
+ <body>
259
+ <h1>⚠️ Client Not Built</h1>
260
+ <p>Run <code>npm run build</code> first.</p>
261
+ <small>Looking at: ${this.clientPath}</small>
262
+ </body>
263
+ </html>
264
+ `);
286
265
  }
287
266
  }
288
- /**
289
- * Get all registered routes
290
- */
291
267
  getRoutes() {
292
268
  return RouteRegistry_1.routeRegistry.getAll();
293
269
  }
294
- /**
295
- * Clear all routes (useful for testing)
296
- */
297
270
  clearRoutes() {
298
271
  RouteRegistry_1.routeRegistry.clear();
299
272
  }
300
- /**
301
- * Get the documentation path
302
- */
303
273
  getDocsPath() {
304
274
  return this.DOCS_PATH;
305
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hasancode-api-docs",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A simple and easy to use API documentation generator for Express.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",