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.
@@ -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="/api-docs/logo.png" />
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="/api-docs/assets/index-CAGuZ--I.js"></script>
17
- <link rel="modulepreload" crossorigin href="/api-docs/assets/vendor-r6CGHiWc.js">
18
- <link rel="stylesheet" crossorigin href="/api-docs/assets/index-BddYm5rR.css">
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
- 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
- });
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
- 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);
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
- 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
- });
110
+ else {
111
+ untagged.push(route);
118
112
  }
119
- return sections;
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
- 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
- },
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
- 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,
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
- 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
+ }
197
+ return section;
183
198
  }
184
- // ══════════════════════════════════════════════════════════════
185
- // FIXED: Static file serving with correct MIME types
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
- // ── 1. Serve config.json (MUST come before static assets) ────
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
- // ── 2. Serve static assets (CSS, JS, etc.) ───────────────────
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, // Don't serve index.html from assets
210
- fallthrough: false, // Return 404 if file not found
225
+ index: false,
226
+ fallthrough: false,
211
227
  }));
212
- // ── 3. Serve React app (catch-all, MUST come last) ───────────
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
- const sections = yield this.convertRoutesToSections();
221
- res.json({
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
- const baseTag = `<base href="${this.DOCS_PATH}/">`;
232
- if (html.includes("<base")) {
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
- <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
- `);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hasancode-api-docs",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
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",