@arikajs/docs 0.0.3 → 0.0.5
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/README.md +12 -5
- package/dist/Drivers/DocDriver.d.ts +6 -0
- package/dist/Drivers/DocDriver.js +2 -0
- package/dist/Generator.d.ts +7 -10
- package/dist/Generator.js +32 -28
- package/dist/HtmlGenerator.d.ts +6 -3
- package/dist/HtmlGenerator.js +277 -82
- package/dist/MarkdownGenerator.d.ts +2 -2
- package/dist/MarkdownGenerator.js +13 -8
- package/dist/OpenApiGenerator.d.ts +2 -4
- package/dist/OpenApiGenerator.js +48 -46
- package/dist/PostmanGenerator.d.ts +3 -3
- package/dist/PostmanGenerator.js +70 -46
- package/dist/RouteParser.d.ts +9 -0
- package/dist/RouteParser.js +178 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +21 -11
- package/dist/types.d.ts +9 -0
- package/dist/types.js +2 -0
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -10,28 +10,35 @@ The goal of this package is to eliminate the manual work of keeping documentatio
|
|
|
10
10
|
|
|
11
11
|
### Status
|
|
12
12
|
|
|
13
|
-
- **Stage**:
|
|
14
|
-
- **
|
|
13
|
+
- **Stage**: Beta / v0.x
|
|
14
|
+
- **Extensibility**: Driver-based architecture (add your own documentation formats)
|
|
15
|
+
- **Features**:
|
|
15
16
|
- Route metadata extraction from `@arikajs/router`
|
|
16
17
|
- Postman Collection (v2.1.0) generation
|
|
17
18
|
- Arika-themed interactive HTML documentation
|
|
18
19
|
- Markdown (DOCS.md) generation
|
|
19
20
|
- OpenAPI 3.0 specification generation
|
|
21
|
+
- **New**: Route filtering by prefix (e.g., generate docs only for `api/v1`)
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
## Features
|
|
24
26
|
|
|
25
|
-
-
|
|
27
|
+
- **🚀 Extensible architecture**
|
|
28
|
+
- Uses a **Driver Pattern** for all documentation formats.
|
|
29
|
+
- Easily add new drivers by implementing the `DocDriver` interface.
|
|
30
|
+
|
|
31
|
+
- **🎨 Multi-Format Generation**
|
|
26
32
|
- **HTML**: A premium, interactive web page for your API.
|
|
27
33
|
- **Postman**: Ready-to-import JSON collection with pre-configured headers.
|
|
28
34
|
- **OpenAPI**: Industry-standard Swagger/OpenAPI 3.0 specification.
|
|
29
35
|
- **Markdown**: Clean, readable `DOCS.md` for GitHub or local documentation.
|
|
30
36
|
|
|
31
|
-
-
|
|
37
|
+
- **🔍 Intelligent Route Analysis**
|
|
38
|
+
- **Prefix Filtering**: Only document specific sections of your app.
|
|
32
39
|
- Automatically groups endpoints by prefix (e.g., `api`, `admin`).
|
|
33
40
|
- Captures route names, methods, and full path hierarchies.
|
|
34
|
-
- Displays middleware
|
|
41
|
+
- Displays middleware and parameter information.
|
|
35
42
|
|
|
36
43
|
- **Environment Support**
|
|
37
44
|
- Automatically generates Postman environment JSON with your `base_url`.
|
package/dist/Generator.d.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { MarkdownGenerator } from './MarkdownGenerator';
|
|
3
|
-
import { HtmlGenerator } from './HtmlGenerator';
|
|
4
|
-
import { OpenApiGenerator } from './OpenApiGenerator';
|
|
1
|
+
import { ParsedRoute } from './types';
|
|
5
2
|
export declare class DocumentationGenerator {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Generate all documentation formats based on parsed routes.
|
|
5
|
+
*/
|
|
6
|
+
generateAll(routes: ParsedRoute[], appName: string, outputDir: string, options?: {
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}): void;
|
|
12
9
|
}
|
package/dist/Generator.js
CHANGED
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DocumentationGenerator = void 0;
|
|
7
|
-
const router_1 = require("@arikajs/router");
|
|
8
7
|
const PostmanGenerator_1 = require("./PostmanGenerator");
|
|
9
8
|
const MarkdownGenerator_1 = require("./MarkdownGenerator");
|
|
10
9
|
const HtmlGenerator_1 = require("./HtmlGenerator");
|
|
@@ -12,41 +11,46 @@ const OpenApiGenerator_1 = require("./OpenApiGenerator");
|
|
|
12
11
|
const fs_1 = __importDefault(require("fs"));
|
|
13
12
|
const path_1 = __importDefault(require("path"));
|
|
14
13
|
class DocumentationGenerator {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.postman = new PostmanGenerator_1.PostmanGenerator();
|
|
21
|
-
this.markdown = new MarkdownGenerator_1.MarkdownGenerator();
|
|
22
|
-
this.html = new HtmlGenerator_1.HtmlGenerator();
|
|
23
|
-
this.openApi = new OpenApiGenerator_1.OpenApiGenerator();
|
|
24
|
-
}
|
|
25
|
-
generateAll(appName, outputDir) {
|
|
26
|
-
const routes = router_1.RouteRegistry.getInstance().getRoutes();
|
|
14
|
+
/**
|
|
15
|
+
* Generate all documentation formats based on parsed routes.
|
|
16
|
+
*/
|
|
17
|
+
generateAll(routes, appName, outputDir, options = {}) {
|
|
18
|
+
const baseUrl = options.baseUrl || 'http://localhost:3000';
|
|
27
19
|
if (!fs_1.default.existsSync(outputDir)) {
|
|
28
20
|
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
29
21
|
}
|
|
30
|
-
// Postman
|
|
31
|
-
const postmanJson = this.postman.generate(routes, appName);
|
|
32
|
-
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'postman_collection.json'), JSON.stringify(postmanJson, null, 2));
|
|
33
|
-
// Environment
|
|
22
|
+
// Generate Postman Environment
|
|
34
23
|
const envJson = {
|
|
35
24
|
name: `${appName} Env`,
|
|
36
25
|
values: [
|
|
37
|
-
{ key: "base_url", value:
|
|
26
|
+
{ key: "base_url", value: baseUrl, enabled: true },
|
|
27
|
+
{ key: "token", value: "", enabled: true }
|
|
38
28
|
]
|
|
39
29
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
fs_1.default.writeFileSync(path_1.default.join(outputDir, '
|
|
47
|
-
// OpenAPI
|
|
48
|
-
const
|
|
49
|
-
|
|
30
|
+
const envContent = JSON.stringify(envJson, null, 2);
|
|
31
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'postman_environment.json'), envContent);
|
|
32
|
+
// Generate Postman Collection
|
|
33
|
+
const postmanGenerator = new PostmanGenerator_1.PostmanGenerator();
|
|
34
|
+
const postmanJson = postmanGenerator.generate(routes, appName, baseUrl);
|
|
35
|
+
const postmanContent = JSON.stringify(postmanJson, null, 2);
|
|
36
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'postman_collection.json'), postmanContent);
|
|
37
|
+
// Generate OpenAPI Spec
|
|
38
|
+
const openApiGenerator = new OpenApiGenerator_1.OpenApiGenerator();
|
|
39
|
+
const openApiJson = openApiGenerator.generate(routes, appName, baseUrl);
|
|
40
|
+
const openApiContent = JSON.stringify(openApiJson, null, 2);
|
|
41
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'openapi.json'), openApiContent);
|
|
42
|
+
// Generate Markdown
|
|
43
|
+
const markdownGenerator = new MarkdownGenerator_1.MarkdownGenerator();
|
|
44
|
+
const markdownContent = markdownGenerator.generate(routes, appName, baseUrl);
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'DOCS.md'), markdownContent);
|
|
46
|
+
// Generate HTML with embedded JSONs
|
|
47
|
+
const htmlGenerator = new HtmlGenerator_1.HtmlGenerator();
|
|
48
|
+
const htmlContent = htmlGenerator.generate(routes, appName, baseUrl, {
|
|
49
|
+
postmanContent,
|
|
50
|
+
envContent,
|
|
51
|
+
openApiContent
|
|
52
|
+
});
|
|
53
|
+
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'api_docs.html'), htmlContent);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
exports.DocumentationGenerator = DocumentationGenerator;
|
package/dist/HtmlGenerator.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParsedRoute } from './types';
|
|
2
2
|
export declare class HtmlGenerator {
|
|
3
|
-
generate(routes:
|
|
4
|
-
|
|
3
|
+
generate(routes: ParsedRoute[], appName: string, baseUrl: string, inlineData: {
|
|
4
|
+
postmanContent: string;
|
|
5
|
+
envContent: string;
|
|
6
|
+
openApiContent: string;
|
|
7
|
+
}): string;
|
|
5
8
|
private groupByPrefix;
|
|
6
9
|
}
|
package/dist/HtmlGenerator.js
CHANGED
|
@@ -2,112 +2,307 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HtmlGenerator = void 0;
|
|
4
4
|
class HtmlGenerator {
|
|
5
|
-
generate(routes, appName) {
|
|
5
|
+
generate(routes, appName, baseUrl, inlineData) {
|
|
6
6
|
const groups = this.groupByPrefix(routes);
|
|
7
|
-
const
|
|
7
|
+
const escapeJs = (s) => s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
8
|
+
const postmanEscaped = escapeJs(inlineData.postmanContent || '{}');
|
|
9
|
+
const envEscaped = escapeJs(inlineData.envContent || '{}');
|
|
10
|
+
const openapiEscaped = escapeJs(inlineData.openApiContent || '{}');
|
|
11
|
+
const methodColors = {
|
|
12
|
+
GET: '#10b981', POST: '#3b82f6', PUT: '#f59e0b', PATCH: '#a78bfa', DELETE: '#ef4444', OPTIONS: '#64748b'
|
|
13
|
+
};
|
|
14
|
+
const sidebarLinks = Object.keys(groups).map(g => {
|
|
15
|
+
const anchor = g.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
16
|
+
return `<li><a href="#${anchor}" onclick="setActive(this)">${g}</a></li>`;
|
|
17
|
+
}).join('\n');
|
|
18
|
+
const groupHtml = Object.entries(groups).map(([groupName, groupRoutes]) => {
|
|
19
|
+
const anchor = groupName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
20
|
+
const routeCards = groupRoutes.map(r => {
|
|
21
|
+
const color = methodColors[r.method] || '#64748b';
|
|
22
|
+
const requiresAuth = r.middleware.includes('auth');
|
|
23
|
+
const mwHtml = r.middleware.map(m => `<span class="mw-badge">${m}</span>`).join('');
|
|
24
|
+
return `<div class="route-card">
|
|
25
|
+
<div class="route-header">
|
|
26
|
+
<span class="method-badge" style="background:${color}">${r.method}</span>
|
|
27
|
+
<code class="route-path">${r.path}</code>
|
|
28
|
+
${requiresAuth ? '<span class="auth-badge">🔒 Auth</span>' : ''}
|
|
29
|
+
${r.name ? `<span class="route-name">${r.name}</span>` : ''}
|
|
30
|
+
</div>
|
|
31
|
+
${(r.handler || r.middleware.length) ? `<div class="route-meta">
|
|
32
|
+
${r.handler ? `<span class="handler-label">⚙ ${r.handler}</span>` : ''}
|
|
33
|
+
${mwHtml}
|
|
34
|
+
</div>` : ''}
|
|
35
|
+
</div>`;
|
|
36
|
+
}).join('\n');
|
|
37
|
+
return `<section class="route-group" id="${anchor}">
|
|
38
|
+
<h2>${groupName}</h2>
|
|
39
|
+
<div class="routes">${routeCards}</div>
|
|
40
|
+
</section>`;
|
|
41
|
+
}).join('\n');
|
|
8
42
|
return `<!DOCTYPE html>
|
|
9
|
-
<html lang="en">
|
|
43
|
+
<html lang="en" data-theme="light">
|
|
10
44
|
<head>
|
|
11
45
|
<meta charset="UTF-8">
|
|
12
46
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
-
<title>${appName} API
|
|
14
|
-
<link
|
|
47
|
+
<title>${appName} | API Docs — ArikaJS</title>
|
|
48
|
+
<link rel="icon" type="image/png" href="./assets/img/favicon.png">
|
|
49
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Plus+Jakarta+Sans:wght@300;400;600;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
15
50
|
<style>
|
|
16
51
|
:root {
|
|
17
52
|
--primary: #8b5cf6;
|
|
53
|
+
--primary-glow: rgba(139, 92, 246, 0.4);
|
|
54
|
+
--secondary: #ec4899;
|
|
18
55
|
--bg: #f8fafc;
|
|
19
|
-
--
|
|
56
|
+
--sidebar-bg: rgba(255,255,255,0.85);
|
|
57
|
+
--card-bg: rgba(255, 255, 255, 0.8);
|
|
58
|
+
--card-border: rgba(15, 23, 42, 0.08);
|
|
20
59
|
--text-main: #0f172a;
|
|
21
60
|
--text-muted: #64748b;
|
|
22
|
-
--border:
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
61
|
+
--border: rgba(15, 23, 42, 0.08);
|
|
62
|
+
--aurora-opacity: 0.3;
|
|
63
|
+
--btn-sec-bg: rgba(15, 23, 42, 0.05);
|
|
64
|
+
--h1-gradient: linear-gradient(135deg, #0f172a 30%, #334155 100%);
|
|
65
|
+
--sidebar-border: rgba(15, 23, 42, 0.08);
|
|
66
|
+
--route-path-bg: rgba(15, 23, 42, 0.05);
|
|
67
|
+
--route-path-color: #1e293b;
|
|
68
|
+
--handler-color: #64748b;
|
|
69
|
+
--mw-bg: rgba(15, 23, 42, 0.05);
|
|
70
|
+
}
|
|
71
|
+
[data-theme="dark"] {
|
|
72
|
+
--bg: #030712;
|
|
73
|
+
--sidebar-bg: rgba(10, 15, 30, 0.9);
|
|
74
|
+
--card-bg: rgba(17, 24, 39, 0.7);
|
|
75
|
+
--card-border: rgba(255, 255, 255, 0.08);
|
|
76
|
+
--text-main: #f8fafc;
|
|
77
|
+
--text-muted: #94a3b8;
|
|
78
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
79
|
+
--aurora-opacity: 0.5;
|
|
80
|
+
--btn-sec-bg: rgba(255, 255, 255, 0.05);
|
|
81
|
+
--h1-gradient: linear-gradient(135deg, #fff 30%, rgba(255, 255, 255, 0.5) 100%);
|
|
82
|
+
--sidebar-border: rgba(255,255,255,0.08);
|
|
83
|
+
--route-path-bg: rgba(255, 255, 255, 0.06);
|
|
84
|
+
--route-path-color: #e2e8f0;
|
|
85
|
+
--handler-color: #475569;
|
|
86
|
+
--mw-bg: rgba(255, 255, 255, 0.06);
|
|
87
|
+
}
|
|
88
|
+
* { margin: 0; padding: 0; box-sizing: border-box; transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; }
|
|
89
|
+
body { font-family: 'Plus Jakarta Sans', sans-serif; background: var(--bg); color: var(--text-main); display: block; min-height: 100vh; overflow-x: hidden; }
|
|
90
|
+
|
|
91
|
+
.aurora { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; filter: blur(100px); opacity: var(--aurora-opacity); pointer-events: none; }
|
|
92
|
+
.aurora-blur { position: absolute; border-radius: 50%; animation: move 20s infinite alternate; }
|
|
93
|
+
.blur-1 { width: 50vw; height: 50vw; background: radial-gradient(circle, var(--primary) 0%, transparent 70%); top: -10%; left: -10%; }
|
|
94
|
+
.blur-2 { width: 40vw; height: 40vw; background: radial-gradient(circle, var(--secondary) 0%, transparent 70%); bottom: -10%; right: -10%; animation-duration: 25s; }
|
|
95
|
+
@keyframes move { from { transform: translate(0,0) scale(1); } to { transform: translate(10%,10%) scale(1.1); } }
|
|
96
|
+
|
|
97
|
+
/* Sidebar */
|
|
98
|
+
.sidebar {
|
|
99
|
+
width: 270px; min-height: 100vh; background: var(--sidebar-bg);
|
|
100
|
+
border-right: 1px solid var(--sidebar-border);
|
|
101
|
+
position: fixed; top: 0; left: 0; overflow-y: auto;
|
|
102
|
+
display: flex; flex-direction: column; z-index: 50;
|
|
103
|
+
backdrop-filter: blur(20px);
|
|
104
|
+
}
|
|
105
|
+
.sidebar-header { padding: 24px 20px 20px; border-bottom: 1px solid var(--sidebar-border); }
|
|
106
|
+
.logo-wrap { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
|
|
107
|
+
.logo-wrap img { width: 110px; height: auto; }
|
|
108
|
+
[data-theme="dark"] .logo-wrap img { filter: invert(1) drop-shadow(0 2px 6px rgba(139,92,246,0.3)); }
|
|
109
|
+
.sidebar-subtitle { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; margin-top: 6px; }
|
|
110
|
+
|
|
111
|
+
.sidebar nav { padding: 16px 0; flex: 1; }
|
|
112
|
+
.sidebar nav h3 { font-size: 10px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .1em; padding: 0 20px 8px; }
|
|
113
|
+
.sidebar nav ul { list-style: none; }
|
|
114
|
+
.sidebar nav ul li a {
|
|
115
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 20px;
|
|
116
|
+
font-size: 13px; color: var(--text-muted); text-decoration: none;
|
|
117
|
+
border-left: 3px solid transparent; transition: all .2s;
|
|
118
|
+
}
|
|
119
|
+
.sidebar nav ul li a:hover, .sidebar nav ul li a.active {
|
|
120
|
+
color: var(--primary); border-left-color: var(--primary); background: rgba(139,92,246,.06);
|
|
121
|
+
}
|
|
122
|
+
.sidebar-footer { padding: 16px 20px; border-top: 1px solid var(--sidebar-border); }
|
|
123
|
+
.sidebar-footer span { display: block; font-size: 11px; color: var(--text-muted); }
|
|
124
|
+
.sidebar-footer strong { color: var(--primary); }
|
|
125
|
+
|
|
126
|
+
/* Header */
|
|
127
|
+
header {
|
|
128
|
+
position: fixed; top: 0; left: 270px; right: 0; min-height: 64px;
|
|
129
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
130
|
+
padding: 12px 32px; background: var(--sidebar-bg); backdrop-filter: blur(20px);
|
|
131
|
+
border-bottom: 1px solid var(--border); z-index: 40;
|
|
27
132
|
}
|
|
133
|
+
.header-title { font-family: 'Outfit', sans-serif; font-size: 16px; font-weight: 700;
|
|
134
|
+
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
135
|
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
136
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
28
137
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
138
|
+
/* Download Buttons */
|
|
139
|
+
.dl-btn {
|
|
140
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
141
|
+
padding: 7px 14px; border-radius: 8px; font-size: 12px; font-weight: 600;
|
|
142
|
+
cursor: pointer; border: 1px solid var(--border); text-decoration: none;
|
|
143
|
+
background: var(--btn-sec-bg); color: var(--text-main);
|
|
144
|
+
transition: all 0.2s cubic-bezier(0.4,0,0.2,1);
|
|
145
|
+
}
|
|
146
|
+
.dl-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(139,92,246,0.2); border-color: var(--primary); color: var(--primary); }
|
|
147
|
+
.dl-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
|
|
148
|
+
.dl-btn.postman { border-color: rgba(255,108,55,0.3); }
|
|
149
|
+
.dl-btn.postman:hover { border-color: #ff6c37; color: #ff6c37; box-shadow: 0 4px 12px rgba(255,108,55,0.2); }
|
|
150
|
+
.dl-btn.openapi { border-color: rgba(16,185,129,0.3); }
|
|
151
|
+
.dl-btn.openapi:hover { border-color: #10b981; color: #10b981; box-shadow: 0 4px 12px rgba(16,185,129,0.2); }
|
|
152
|
+
|
|
153
|
+
/* Theme Toggle */
|
|
154
|
+
.theme-toggle {
|
|
155
|
+
background: var(--card-bg); border: 1px solid var(--border);
|
|
156
|
+
padding: 7px; border-radius: 99px; cursor: pointer;
|
|
157
|
+
display: flex; align-items: center; gap: 4px;
|
|
158
|
+
}
|
|
159
|
+
.theme-toggle svg { width: 18px; height: 18px; color: var(--text-main); transition: color 0.3s; }
|
|
160
|
+
|
|
161
|
+
/* Main */
|
|
162
|
+
.main { margin-left: 270px; margin-top: 72px; padding: 40px; position: relative; z-index: 10; }
|
|
163
|
+
|
|
164
|
+
/* Route Group */
|
|
165
|
+
.route-group { width: 100%; margin-bottom: 56px; scroll-margin-top: 80px; }
|
|
166
|
+
.route-group h2 {
|
|
167
|
+
font-family: 'Outfit', sans-serif; font-size: 18px; font-weight: 700;
|
|
168
|
+
color: var(--text-main); margin-bottom: 16px;
|
|
169
|
+
display: flex; align-items: center; gap: 10px;
|
|
170
|
+
}
|
|
171
|
+
.route-group h2::before { content: ''; width: 4px; height: 20px; background: linear-gradient(var(--primary), var(--secondary)); border-radius: 2px; flex-shrink: 0; }
|
|
172
|
+
.routes { display: flex; flex-direction: column; gap: 10px; width: 100%; }
|
|
173
|
+
|
|
174
|
+
/* Route Card */
|
|
175
|
+
.route-card {
|
|
176
|
+
width: 100%; background: var(--card-bg); border: 1px solid var(--card-border);
|
|
177
|
+
border-radius: 14px; padding: 16px 20px;
|
|
178
|
+
transition: border-color .25s, transform .25s, box-shadow .25s;
|
|
179
|
+
backdrop-filter: blur(10px);
|
|
180
|
+
}
|
|
181
|
+
.route-card:hover { border-color: var(--primary); transform: translateX(4px); box-shadow: 0 8px 30px var(--primary-glow); }
|
|
182
|
+
.route-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
183
|
+
.method-badge {
|
|
184
|
+
padding: 4px 14px; border-radius: 6px; font-family: 'JetBrains Mono', monospace;
|
|
185
|
+
font-size: 11px; font-weight: 700; color: #fff; min-width: 72px; text-align: center; letter-spacing: .05em;
|
|
186
|
+
}
|
|
187
|
+
.route-path {
|
|
188
|
+
font-family: 'JetBrains Mono', monospace; font-size: 14px;
|
|
189
|
+
color: var(--route-path-color); background: var(--route-path-bg);
|
|
190
|
+
padding: 4px 10px; border-radius: 6px; border: 1px solid var(--border);
|
|
191
|
+
}
|
|
192
|
+
.auth-badge { font-size: 11px; color: #f59e0b; background: rgba(245,158,11,.1); padding: 3px 8px; border-radius: 4px; border: 1px solid rgba(245,158,11,.2); }
|
|
193
|
+
.route-name { font-size: 11px; color: var(--text-muted); margin-left: auto; font-family: 'JetBrains Mono', monospace; }
|
|
194
|
+
.route-meta { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
|
|
195
|
+
.handler-label { font-size: 12px; color: var(--handler-color); font-family: 'JetBrains Mono', monospace; }
|
|
196
|
+
.mw-badge { padding: 2px 8px; background: var(--mw-bg); border-radius: 4px; font-size: 11px; color: var(--text-muted); border: 1px solid var(--border); }
|
|
57
197
|
</style>
|
|
58
198
|
</head>
|
|
59
199
|
<body>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</
|
|
200
|
+
|
|
201
|
+
<div class="aurora">
|
|
202
|
+
<div class="aurora-blur blur-1"></div>
|
|
203
|
+
<div class="aurora-blur blur-2"></div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<aside class="sidebar">
|
|
207
|
+
<div class="sidebar-header">
|
|
208
|
+
<div class="logo-wrap">
|
|
209
|
+
<img src="./assets/img/logo.png" alt="ArikaJS">
|
|
210
|
+
</div>
|
|
211
|
+
<div class="sidebar-subtitle">API Documentation</div>
|
|
212
|
+
</div>
|
|
213
|
+
<nav>
|
|
214
|
+
<h3>Sections</h3>
|
|
215
|
+
<ul id="nav-links">
|
|
216
|
+
${sidebarLinks}
|
|
217
|
+
</ul>
|
|
218
|
+
</nav>
|
|
219
|
+
<div class="sidebar-footer">
|
|
220
|
+
<span>Total Routes: <strong>${routes.length}</strong></span>
|
|
221
|
+
<span style="margin-top:4px">Generated: <strong>${new Date().toLocaleDateString()}</strong></span>
|
|
222
|
+
</div>
|
|
223
|
+
</aside>
|
|
224
|
+
|
|
225
|
+
<header>
|
|
226
|
+
<div class="header-left">
|
|
227
|
+
<div class="header-title">${appName} — API Reference</div>
|
|
228
|
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">Complete API documentation — auto-generated from your ArikaJS route definitions.</div>
|
|
72
229
|
</div>
|
|
230
|
+
<div class="header-right">
|
|
231
|
+
<button class="dl-btn postman" id="btn-postman" title="Download Postman Collection">
|
|
232
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
233
|
+
Postman
|
|
234
|
+
</button>
|
|
235
|
+
<button class="dl-btn" id="btn-env" title="Download Postman Environment">
|
|
236
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
237
|
+
Environment
|
|
238
|
+
</button>
|
|
239
|
+
<button class="dl-btn openapi" id="btn-openapi" title="Download OpenAPI Spec">
|
|
240
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
241
|
+
OpenAPI
|
|
242
|
+
</button>
|
|
243
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle Theme">
|
|
244
|
+
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
245
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
|
246
|
+
</svg>
|
|
247
|
+
<svg id="sun-icon" style="display:none" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
248
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
249
|
+
</svg>
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</header>
|
|
253
|
+
|
|
254
|
+
<main class="main">
|
|
255
|
+
${groupHtml}
|
|
256
|
+
</main>
|
|
257
|
+
|
|
258
|
+
<script>
|
|
259
|
+
const html = document.documentElement;
|
|
260
|
+
const toggle = document.getElementById('theme-toggle');
|
|
261
|
+
const sun = document.getElementById('sun-icon');
|
|
262
|
+
const moon = document.getElementById('moon-icon');
|
|
263
|
+
|
|
264
|
+
function setTheme(t) {
|
|
265
|
+
html.setAttribute('data-theme', t);
|
|
266
|
+
localStorage.setItem('arika_theme', t);
|
|
267
|
+
if (t === 'dark') { sun.style.display = 'block'; moon.style.display = 'none'; }
|
|
268
|
+
else { sun.style.display = 'none'; moon.style.display = 'block'; }
|
|
269
|
+
}
|
|
270
|
+
setTheme(localStorage.getItem('arika_theme') || 'light');
|
|
271
|
+
toggle.addEventListener('click', () => setTheme(html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'));
|
|
272
|
+
|
|
273
|
+
function setActive(el) {
|
|
274
|
+
document.querySelectorAll('#nav-links a').forEach(a => a.classList.remove('active'));
|
|
275
|
+
el.classList.add('active');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function download(filename, content) {
|
|
279
|
+
const blob = new Blob([content], { type: 'application/json' });
|
|
280
|
+
const url = URL.createObjectURL(blob);
|
|
281
|
+
const a = document.createElement('a');
|
|
282
|
+
a.href = url; a.download = filename; a.click();
|
|
283
|
+
URL.revokeObjectURL(url);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const postmanData = \`${postmanEscaped}\`;
|
|
287
|
+
const envData = \`${envEscaped}\`;
|
|
288
|
+
const openapiData = \`${openapiEscaped}\`;
|
|
289
|
+
|
|
290
|
+
document.getElementById('btn-postman').addEventListener('click', () => download('postman_collection.json', postmanData));
|
|
291
|
+
document.getElementById('btn-env').addEventListener('click', () => download('postman_environment.json', envData));
|
|
292
|
+
document.getElementById('btn-openapi').addEventListener('click', () => download('openapi.json', openapiData));
|
|
293
|
+
</script>
|
|
73
294
|
</body>
|
|
74
295
|
</html>`;
|
|
75
296
|
}
|
|
76
|
-
renderGroup(name, routes) {
|
|
77
|
-
const routesHtml = routes.map(route => `
|
|
78
|
-
<div class="route-card">
|
|
79
|
-
<div class="route-header">
|
|
80
|
-
<span class="method ${route.method}">${route.method}</span>
|
|
81
|
-
<span class="path">${route.path}</span>
|
|
82
|
-
</div>
|
|
83
|
-
<div class="route-info">
|
|
84
|
-
<div>
|
|
85
|
-
<div class="label">Route Name</div>
|
|
86
|
-
<div class="value">${route.name || '-'}</div>
|
|
87
|
-
</div>
|
|
88
|
-
<div>
|
|
89
|
-
<div class="label">Middleware</div>
|
|
90
|
-
<div class="value">
|
|
91
|
-
${route.middleware.map(m => `<span class="middleware-tag">${typeof m === 'string' ? m : m.name || 'Closure'}</span>`).join('') || '-'}
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
`).join('');
|
|
97
|
-
return `
|
|
98
|
-
<div class="group">
|
|
99
|
-
<h2 class="group-title">${name.replace('/', '')}</h2>
|
|
100
|
-
${routesHtml}
|
|
101
|
-
</div>
|
|
102
|
-
`;
|
|
103
|
-
}
|
|
104
297
|
groupByPrefix(routes) {
|
|
105
298
|
const groups = {};
|
|
106
|
-
routes.forEach(
|
|
107
|
-
|
|
299
|
+
routes.forEach(r => {
|
|
300
|
+
let group = r.prefix ? r.prefix.replace(/^\/+/, '').split('/').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' / ') : 'General';
|
|
301
|
+
if (!group)
|
|
302
|
+
group = 'General';
|
|
108
303
|
if (!groups[group])
|
|
109
304
|
groups[group] = [];
|
|
110
|
-
groups[group].push(
|
|
305
|
+
groups[group].push(r);
|
|
111
306
|
});
|
|
112
307
|
return groups;
|
|
113
308
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParsedRoute } from './types';
|
|
2
2
|
export declare class MarkdownGenerator {
|
|
3
|
-
generate(routes:
|
|
3
|
+
generate(routes: ParsedRoute[], appName: string, baseUrl?: string): string;
|
|
4
4
|
private groupByPrefix;
|
|
5
5
|
}
|
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MarkdownGenerator = void 0;
|
|
4
4
|
class MarkdownGenerator {
|
|
5
|
-
generate(routes, appName) {
|
|
5
|
+
generate(routes, appName, baseUrl) {
|
|
6
6
|
let markdown = `# API Documentation: ${appName}\n\n`;
|
|
7
7
|
markdown += `Generated on ${new Date().toLocaleDateString()}\n\n`;
|
|
8
|
+
markdown += `> **Total Routes:** ${routes.length}\n\n`;
|
|
8
9
|
const groups = this.groupByPrefix(routes);
|
|
9
10
|
for (const [group, groupRoutes] of Object.entries(groups)) {
|
|
10
11
|
markdown += `## ${group}\n\n`;
|
|
11
|
-
markdown += `| Method | Path | Name | Middleware |\n`;
|
|
12
|
-
markdown += `| :--- | :--- | :--- | :--- |\n`;
|
|
12
|
+
markdown += `| Method | Path | Handler | Name | Middleware |\n`;
|
|
13
|
+
markdown += `| :--- | :--- | :--- | :--- | :--- |\n`;
|
|
13
14
|
groupRoutes.forEach(route => {
|
|
14
|
-
const middleware = route.middleware.
|
|
15
|
-
|
|
15
|
+
const middleware = route.middleware.length > 0 ? route.middleware.join(', ') : '-';
|
|
16
|
+
const handler = route.handler || '-';
|
|
17
|
+
const name = route.name || '-';
|
|
18
|
+
markdown += `| **${route.method}** | \`${route.path}\` | ${handler} | ${name} | ${middleware} |\n`;
|
|
16
19
|
});
|
|
17
20
|
markdown += `\n`;
|
|
18
21
|
}
|
|
@@ -20,11 +23,13 @@ class MarkdownGenerator {
|
|
|
20
23
|
}
|
|
21
24
|
groupByPrefix(routes) {
|
|
22
25
|
const groups = {};
|
|
23
|
-
routes.forEach(
|
|
24
|
-
|
|
26
|
+
routes.forEach(r => {
|
|
27
|
+
let group = r.prefix ? r.prefix.replace(/^\/+/, '').split('/').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' / ') : 'General';
|
|
28
|
+
if (!group)
|
|
29
|
+
group = 'General';
|
|
25
30
|
if (!groups[group])
|
|
26
31
|
groups[group] = [];
|
|
27
|
-
groups[group].push(
|
|
32
|
+
groups[group].push(r);
|
|
28
33
|
});
|
|
29
34
|
return groups;
|
|
30
35
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParsedRoute } from './types';
|
|
2
2
|
export declare class OpenApiGenerator {
|
|
3
|
-
generate(routes:
|
|
4
|
-
private formatPaths;
|
|
5
|
-
private extractParameters;
|
|
3
|
+
generate(routes: ParsedRoute[], appName: string, baseUrl: string): any;
|
|
6
4
|
}
|
package/dist/OpenApiGenerator.js
CHANGED
|
@@ -2,63 +2,65 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OpenApiGenerator = void 0;
|
|
4
4
|
class OpenApiGenerator {
|
|
5
|
-
generate(routes, appName) {
|
|
5
|
+
generate(routes, appName, baseUrl) {
|
|
6
|
+
const paths = {};
|
|
7
|
+
routes.forEach(route => {
|
|
8
|
+
const rawPath = route.path.replace(/^\/+/, '');
|
|
9
|
+
const openApiPath = '/' + rawPath.replace(/:([a-zA-Z0-9_]+)|\{([a-zA-Z0-9_]+)\}/g, '{$1$2}');
|
|
10
|
+
if (!paths[openApiPath])
|
|
11
|
+
paths[openApiPath] = {};
|
|
12
|
+
const method = route.method.toLowerCase();
|
|
13
|
+
const group = route.prefix ? route.prefix.replace(/^\/+/, '').split('/').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' / ') : 'General';
|
|
14
|
+
const routeName = route.name || `${method}_${rawPath.replace(/\//g, '_')}`;
|
|
15
|
+
const parameters = route.paramKeys.map(key => ({
|
|
16
|
+
name: key,
|
|
17
|
+
in: 'path',
|
|
18
|
+
required: true,
|
|
19
|
+
schema: { type: 'string' }
|
|
20
|
+
}));
|
|
21
|
+
const operation = {
|
|
22
|
+
summary: routeName,
|
|
23
|
+
tags: [group || 'General'],
|
|
24
|
+
parameters: parameters.length > 0 ? parameters : undefined,
|
|
25
|
+
responses: {
|
|
26
|
+
'200': { description: 'Successful response' },
|
|
27
|
+
'401': route.middleware.includes('auth') ? { description: 'Unauthorized' } : undefined,
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
if (route.middleware.includes('auth')) {
|
|
31
|
+
operation.security = [{ bearerAuth: [] }];
|
|
32
|
+
}
|
|
33
|
+
if (['post', 'put', 'patch'].includes(method)) {
|
|
34
|
+
operation.requestBody = {
|
|
35
|
+
content: {
|
|
36
|
+
'application/json': {
|
|
37
|
+
schema: { type: 'object', properties: {} }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
paths[openApiPath][method] = operation;
|
|
43
|
+
});
|
|
44
|
+
const servers = [{ url: baseUrl }];
|
|
6
45
|
return {
|
|
7
|
-
openapi:
|
|
46
|
+
openapi: '3.0.0',
|
|
8
47
|
info: {
|
|
9
48
|
title: appName,
|
|
10
|
-
|
|
11
|
-
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
description: `API documentation for ${appName}`
|
|
12
51
|
},
|
|
13
|
-
|
|
52
|
+
servers,
|
|
53
|
+
paths,
|
|
14
54
|
components: {
|
|
15
|
-
schemas: {},
|
|
16
55
|
securitySchemes: {
|
|
17
56
|
bearerAuth: {
|
|
18
|
-
type:
|
|
19
|
-
scheme:
|
|
20
|
-
bearerFormat:
|
|
57
|
+
type: 'http',
|
|
58
|
+
scheme: 'bearer',
|
|
59
|
+
bearerFormat: 'JWT'
|
|
21
60
|
}
|
|
22
61
|
}
|
|
23
62
|
}
|
|
24
63
|
};
|
|
25
64
|
}
|
|
26
|
-
formatPaths(routes) {
|
|
27
|
-
const paths = {};
|
|
28
|
-
routes.forEach(route => {
|
|
29
|
-
const path = route.path.replace(/:([a-zA-Z0-9_]+)|\{([a-zA-Z0-9_]+)\}/g, '{$1$2}');
|
|
30
|
-
if (!paths[path]) {
|
|
31
|
-
paths[path] = {};
|
|
32
|
-
}
|
|
33
|
-
paths[path][route.method.toLowerCase()] = {
|
|
34
|
-
summary: route.name || `Endpoint for ${path}`,
|
|
35
|
-
tags: [route.prefix || 'General'],
|
|
36
|
-
parameters: this.extractParameters(route),
|
|
37
|
-
responses: {
|
|
38
|
-
"200": {
|
|
39
|
-
description: "Successful response",
|
|
40
|
-
content: {
|
|
41
|
-
"application/json": {
|
|
42
|
-
schema: {
|
|
43
|
-
type: "object"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
return paths;
|
|
52
|
-
}
|
|
53
|
-
extractParameters(route) {
|
|
54
|
-
return route.paramKeys.map(key => ({
|
|
55
|
-
name: key,
|
|
56
|
-
in: "path",
|
|
57
|
-
required: true,
|
|
58
|
-
schema: {
|
|
59
|
-
type: "string"
|
|
60
|
-
}
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
65
|
}
|
|
64
66
|
exports.OpenApiGenerator = OpenApiGenerator;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParsedRoute } from './types';
|
|
2
2
|
export declare class PostmanGenerator {
|
|
3
|
-
generate(routes:
|
|
4
|
-
private
|
|
3
|
+
generate(routes: ParsedRoute[], appName: string, baseUrl: string): any;
|
|
4
|
+
private groupByPrefix;
|
|
5
5
|
}
|
package/dist/PostmanGenerator.js
CHANGED
|
@@ -2,59 +2,83 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PostmanGenerator = void 0;
|
|
4
4
|
class PostmanGenerator {
|
|
5
|
-
generate(routes, appName) {
|
|
5
|
+
generate(routes, appName, baseUrl) {
|
|
6
|
+
const groups = this.groupByPrefix(routes);
|
|
7
|
+
const item = [];
|
|
8
|
+
for (const [groupName, groupRoutes] of Object.entries(groups)) {
|
|
9
|
+
const groupItem = {
|
|
10
|
+
name: groupName,
|
|
11
|
+
item: []
|
|
12
|
+
};
|
|
13
|
+
groupRoutes.forEach(route => {
|
|
14
|
+
const requestName = route.name || `${route.method} ${route.path}`;
|
|
15
|
+
// Parse params out of path
|
|
16
|
+
const parts = route.path.replace(/^\/+/, '').split('/');
|
|
17
|
+
const urlPath = parts.map(p => {
|
|
18
|
+
const match = p.match(/^:([a-zA-Z0-9_]+)$/) || p.match(/^\{([a-zA-Z0-9_]+)\}$/);
|
|
19
|
+
if (match)
|
|
20
|
+
return `:${match[1]}`;
|
|
21
|
+
return p;
|
|
22
|
+
});
|
|
23
|
+
const variables = route.paramKeys.map(key => ({
|
|
24
|
+
key, value: `<string>`
|
|
25
|
+
}));
|
|
26
|
+
const req = {
|
|
27
|
+
name: requestName,
|
|
28
|
+
request: {
|
|
29
|
+
method: route.method,
|
|
30
|
+
header: [
|
|
31
|
+
{ key: 'Accept', value: 'application/json', type: 'text' },
|
|
32
|
+
{ key: 'Content-Type', value: 'application/json', type: 'text' }
|
|
33
|
+
],
|
|
34
|
+
url: {
|
|
35
|
+
raw: `{{base_url}}/${urlPath.join('/')}`,
|
|
36
|
+
host: ['{{base_url}}'],
|
|
37
|
+
path: urlPath,
|
|
38
|
+
variable: variables.length > 0 ? variables : undefined
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
response: []
|
|
42
|
+
};
|
|
43
|
+
if (route.middleware.includes('auth')) {
|
|
44
|
+
req.request.auth = {
|
|
45
|
+
type: 'bearer',
|
|
46
|
+
bearer: [
|
|
47
|
+
{ key: "token", value: "{{token}}", type: "string" }
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (['POST', 'PUT', 'PATCH'].includes(route.method)) {
|
|
52
|
+
req.request.body = {
|
|
53
|
+
mode: 'raw',
|
|
54
|
+
raw: JSON.stringify({}, null, 2),
|
|
55
|
+
options: { raw: { language: 'json' } }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
groupItem.item.push(req);
|
|
59
|
+
});
|
|
60
|
+
item.push(groupItem);
|
|
61
|
+
}
|
|
6
62
|
return {
|
|
7
63
|
info: {
|
|
8
|
-
name: appName
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
64
|
+
name: `${appName} API`,
|
|
65
|
+
description: `Auto-generated API collection for ${appName}`,
|
|
66
|
+
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
|
12
67
|
},
|
|
13
|
-
item
|
|
14
|
-
variable: [
|
|
15
|
-
{
|
|
16
|
-
key: "base_url",
|
|
17
|
-
value: "http://localhost:3000",
|
|
18
|
-
type: "string"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
68
|
+
item
|
|
21
69
|
};
|
|
22
70
|
}
|
|
23
|
-
|
|
24
|
-
// Group routes by prefix/module
|
|
71
|
+
groupByPrefix(routes) {
|
|
25
72
|
const groups = {};
|
|
26
|
-
routes.forEach(
|
|
27
|
-
|
|
28
|
-
if (!
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
request: {
|
|
34
|
-
method: route.method,
|
|
35
|
-
header: [
|
|
36
|
-
{
|
|
37
|
-
key: "Content-Type",
|
|
38
|
-
value: "application/json"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
key: "Accept",
|
|
42
|
-
value: "application/json"
|
|
43
|
-
}
|
|
44
|
-
],
|
|
45
|
-
url: {
|
|
46
|
-
raw: "{{base_url}}" + route.path,
|
|
47
|
-
host: ["{{base_url}}"],
|
|
48
|
-
path: route.path.split('/').filter(p => p !== '')
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
response: []
|
|
52
|
-
});
|
|
73
|
+
routes.forEach(r => {
|
|
74
|
+
let group = r.prefix ? r.prefix.replace(/^\/+/, '').split('/').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' / ') : 'General';
|
|
75
|
+
if (!group)
|
|
76
|
+
group = 'General';
|
|
77
|
+
if (!groups[group])
|
|
78
|
+
groups[group] = [];
|
|
79
|
+
groups[group].push(r);
|
|
53
80
|
});
|
|
54
|
-
return
|
|
55
|
-
name: name.charAt(0).toUpperCase() + name.slice(1).replace('/', ''),
|
|
56
|
-
item: groups[name]
|
|
57
|
-
}));
|
|
81
|
+
return groups;
|
|
58
82
|
}
|
|
59
83
|
}
|
|
60
84
|
exports.PostmanGenerator = PostmanGenerator;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class RouteParser {
|
|
2
|
+
parseApplicationRoutes(appDir: string, routeFileName?: string | null): any[];
|
|
3
|
+
private extractRoutesFromContent;
|
|
4
|
+
private extractGroupRoutes;
|
|
5
|
+
private stripGroupBlocks;
|
|
6
|
+
private extractFlatRoutes;
|
|
7
|
+
private joinPaths;
|
|
8
|
+
private extractParamKeys;
|
|
9
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RouteParser = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
class RouteParser {
|
|
10
|
+
parseApplicationRoutes(appDir, routeFileName = null) {
|
|
11
|
+
const routesDir = path_1.default.join(appDir, 'routes');
|
|
12
|
+
if (!fs_1.default.existsSync(routesDir))
|
|
13
|
+
return [];
|
|
14
|
+
const routes = [];
|
|
15
|
+
let filesToParse = [];
|
|
16
|
+
if (routeFileName) {
|
|
17
|
+
const specificFile = path_1.default.join(routesDir, routeFileName.endsWith('.ts') ? routeFileName : `${routeFileName}.ts`);
|
|
18
|
+
if (fs_1.default.existsSync(specificFile)) {
|
|
19
|
+
filesToParse = [specificFile];
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.warn(`\n ⚠ Route file not found: ${specificFile}\n`);
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const allFiles = fs_1.default.readdirSync(routesDir).filter(f => f.endsWith('.ts'));
|
|
28
|
+
if (allFiles.includes('api.ts')) {
|
|
29
|
+
filesToParse = [path_1.default.join(routesDir, 'api.ts')];
|
|
30
|
+
}
|
|
31
|
+
else if (allFiles.length > 0) {
|
|
32
|
+
filesToParse = [path_1.default.join(routesDir, allFiles[0])];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const file of filesToParse) {
|
|
36
|
+
const content = fs_1.default.readFileSync(file, 'utf8');
|
|
37
|
+
this.extractRoutesFromContent(content, routes);
|
|
38
|
+
}
|
|
39
|
+
return routes;
|
|
40
|
+
}
|
|
41
|
+
extractRoutesFromContent(content, routes) {
|
|
42
|
+
const strippedContent = this.extractGroupRoutes(content, routes);
|
|
43
|
+
this.extractFlatRoutes(strippedContent, routes, '', []);
|
|
44
|
+
}
|
|
45
|
+
extractGroupRoutes(content, routes) {
|
|
46
|
+
const groupRegex = /Route\.group\s*\(\s*\{([^}]+)\}\s*,\s*\(\s*\)\s*=>\s*\{([\s\S]*?)\}\s*\)/g;
|
|
47
|
+
let match;
|
|
48
|
+
let contentCopy = content;
|
|
49
|
+
while ((match = groupRegex.exec(content)) !== null) {
|
|
50
|
+
const groupConfig = match[1];
|
|
51
|
+
const groupBody = match[2];
|
|
52
|
+
let prefix = '';
|
|
53
|
+
let middleware = [];
|
|
54
|
+
const prefixMatch = groupConfig.match(/prefix\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
55
|
+
if (prefixMatch)
|
|
56
|
+
prefix = prefixMatch[1];
|
|
57
|
+
const mwMatch = groupConfig.match(/middleware\s*:\s*(['"`][^'"`]+['"`]|\[[^\]]+\])/);
|
|
58
|
+
if (mwMatch) {
|
|
59
|
+
const mwStr = mwMatch[1];
|
|
60
|
+
if (mwStr.startsWith('[')) {
|
|
61
|
+
const mws = mwStr.match(/['"`]([^'"`]+)['"`]/g);
|
|
62
|
+
if (mws)
|
|
63
|
+
middleware = mws.map(m => m.replace(/['"`]/g, ''));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
middleware.push(mwStr.replace(/['"`]/g, ''));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.extractFlatRoutes(groupBody, routes, prefix, middleware);
|
|
70
|
+
}
|
|
71
|
+
return this.stripGroupBlocks(contentCopy);
|
|
72
|
+
}
|
|
73
|
+
stripGroupBlocks(content) {
|
|
74
|
+
let result = content;
|
|
75
|
+
let changed = true;
|
|
76
|
+
while (changed) {
|
|
77
|
+
changed = false;
|
|
78
|
+
const groupStart = result.indexOf('Route.group(');
|
|
79
|
+
if (groupStart === -1)
|
|
80
|
+
break;
|
|
81
|
+
const arrowIdx = result.indexOf('=>', groupStart);
|
|
82
|
+
if (arrowIdx === -1)
|
|
83
|
+
break;
|
|
84
|
+
const braceStart = result.indexOf('{', arrowIdx);
|
|
85
|
+
if (braceStart === -1)
|
|
86
|
+
break;
|
|
87
|
+
let depth = 0;
|
|
88
|
+
let i = braceStart;
|
|
89
|
+
for (; i < result.length; i++) {
|
|
90
|
+
if (result[i] === '{')
|
|
91
|
+
depth++;
|
|
92
|
+
else if (result[i] === '}') {
|
|
93
|
+
depth--;
|
|
94
|
+
if (depth === 0)
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let endIdx = result.indexOf(';', i);
|
|
99
|
+
if (endIdx === -1)
|
|
100
|
+
endIdx = i + 1;
|
|
101
|
+
result = result.substring(0, groupStart) + result.substring(endIdx + 1);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
extractFlatRoutes(content, routes, prefix, middleware) {
|
|
107
|
+
const normalized = content.replace(/\s+/g, ' ');
|
|
108
|
+
const routeRegex = /Route\.(get|post|put|patch|delete|options)\s*\(\s*['"`]([^'"`]*)['"`]\s*,\s*((?:[^\(\)]*|\([^\)]*\))*)\)\s*([^;]*)?;?/g;
|
|
109
|
+
let match;
|
|
110
|
+
while ((match = routeRegex.exec(normalized)) !== null) {
|
|
111
|
+
const method = match[1].toUpperCase();
|
|
112
|
+
const routePath = match[2];
|
|
113
|
+
const handlerStr = match[3]?.trim() || '';
|
|
114
|
+
const modifiersStr = match[4] || '';
|
|
115
|
+
const controllerMatch = handlerStr.match(/\[\s*(\w+)\s*,\s*['"`](\w+)['"`]\s*\]/);
|
|
116
|
+
const instanceMatch = handlerStr.match(/(\w+)\.(\w+)/);
|
|
117
|
+
let handlerLabel;
|
|
118
|
+
if (controllerMatch) {
|
|
119
|
+
handlerLabel = `${controllerMatch[1]}@${controllerMatch[2]}`;
|
|
120
|
+
}
|
|
121
|
+
else if (instanceMatch && !handlerStr.includes('=>')) {
|
|
122
|
+
handlerLabel = `${instanceMatch[1]}.${instanceMatch[2]}`;
|
|
123
|
+
}
|
|
124
|
+
else if (handlerStr.includes('=>')) {
|
|
125
|
+
handlerLabel = '[Closure]';
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
handlerLabel = handlerStr.substring(0, 40);
|
|
129
|
+
}
|
|
130
|
+
const routeMiddleware = [...middleware];
|
|
131
|
+
const withMwRegex = /\.withMiddleware\s*\(\s*(['"`][^'"`]+['"`]|\[[^\]]+\])\s*\)/;
|
|
132
|
+
const withMwMatch = modifiersStr.match(withMwRegex);
|
|
133
|
+
if (withMwMatch) {
|
|
134
|
+
const mwStr = withMwMatch[1];
|
|
135
|
+
if (mwStr.startsWith('[')) {
|
|
136
|
+
const mws = mwStr.match(/['"`]([^'"`]+)['"`]/g);
|
|
137
|
+
if (mws)
|
|
138
|
+
routeMiddleware.push(...mws.map(m => m.replace(/['"`]/g, '')));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
routeMiddleware.push(mwStr.replace(/['"`]/g, ''));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let name;
|
|
145
|
+
const asRegex = /\.as\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/;
|
|
146
|
+
const asMatch = modifiersStr.match(asRegex);
|
|
147
|
+
if (asMatch)
|
|
148
|
+
name = asMatch[1];
|
|
149
|
+
const fullPath = this.joinPaths(prefix, routePath);
|
|
150
|
+
const paramKeys = this.extractParamKeys(fullPath);
|
|
151
|
+
routes.push({
|
|
152
|
+
method,
|
|
153
|
+
path: fullPath,
|
|
154
|
+
handler: handlerLabel !== '[Closure]' && handlerLabel.length > 0 ? handlerLabel : undefined,
|
|
155
|
+
name,
|
|
156
|
+
middleware: routeMiddleware,
|
|
157
|
+
prefix,
|
|
158
|
+
paramKeys
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
joinPaths(prefix, path) {
|
|
163
|
+
const cleanPrefix = prefix.replace(/^\/+|\/+$/g, '');
|
|
164
|
+
const cleanPath = path.replace(/^\/+|\/+$/g, '');
|
|
165
|
+
const sep = cleanPrefix && cleanPath ? '/' : '';
|
|
166
|
+
return `/${cleanPrefix}${sep}${cleanPath}`;
|
|
167
|
+
}
|
|
168
|
+
extractParamKeys(pathStr) {
|
|
169
|
+
const keys = [];
|
|
170
|
+
const regex = /:([a-zA-Z0-9_]+)|\{([a-zA-Z0-9_]+)\}/g;
|
|
171
|
+
let match;
|
|
172
|
+
while ((match = regex.exec(pathStr)) !== null) {
|
|
173
|
+
keys.push(match[1] || match[2]);
|
|
174
|
+
}
|
|
175
|
+
return keys;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.RouteParser = RouteParser;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './RouteParser';
|
|
3
|
+
export * from './Generator';
|
|
4
|
+
export * from './MarkdownGenerator';
|
|
5
|
+
export * from './PostmanGenerator';
|
|
6
|
+
export * from './OpenApiGenerator';
|
|
7
|
+
export * from './HtmlGenerator';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var HtmlGenerator_1 = require("./HtmlGenerator");
|
|
11
|
-
Object.defineProperty(exports, "HtmlGenerator", { enumerable: true, get: function () { return HtmlGenerator_1.HtmlGenerator; } });
|
|
12
|
-
var OpenApiGenerator_1 = require("./OpenApiGenerator");
|
|
13
|
-
Object.defineProperty(exports, "OpenApiGenerator", { enumerable: true, get: function () { return OpenApiGenerator_1.OpenApiGenerator; } });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./RouteParser"), exports);
|
|
19
|
+
__exportStar(require("./Generator"), exports);
|
|
20
|
+
__exportStar(require("./MarkdownGenerator"), exports);
|
|
21
|
+
__exportStar(require("./PostmanGenerator"), exports);
|
|
22
|
+
__exportStar(require("./OpenApiGenerator"), exports);
|
|
23
|
+
__exportStar(require("./HtmlGenerator"), exports);
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arikajs/docs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "API documentation and Postman collection generator for ArikaJS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc -p tsconfig.json",
|
|
10
10
|
"clean": "rm -rf dist",
|
|
11
|
-
"test": "npm run build && node --test 'dist/tests/**/*.test.js'"
|
|
11
|
+
"test": "npm run build && node --test 'dist/tests/**/*.test.js'",
|
|
12
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"dist"
|
|
@@ -22,11 +23,20 @@
|
|
|
22
23
|
"documentation"
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@arikajs/router": "
|
|
26
|
+
"@arikajs/router": "*"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^20.11.24",
|
|
29
30
|
"typescript": "^5.3.3"
|
|
30
31
|
},
|
|
31
|
-
"author": "Prakash Tank"
|
|
32
|
+
"author": "Prakash Tank",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/ArikaJs/arikajs.git",
|
|
36
|
+
"directory": "packages/docs"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/ArikaJs/arikajs/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/ArikaJs/arikajs/tree/main/packages/docs#readme"
|
|
32
42
|
}
|