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.
@@ -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,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
- 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. Serve config.json
190
206
  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
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, // Don't serve index.html from assets
210
- fallthrough: false, // Return 404 if file not found
220
+ index: false,
221
+ fallthrough: false,
211
222
  }));
212
- // ── 3. Serve React app (catch-all, MUST come last) ───────────
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
- const sections = yield this.convertRoutesToSections();
221
- res.json({
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
- const baseTag = `<base href="${this.DOCS_PATH}/">`;
232
- if (html.includes("<base")) {
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
- <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
- `);
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
  }
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.10",
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",