froggy-docs 1.0.8 → 1.1.0

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/bin/froggy-docs CHANGED
Binary file
@@ -14,6 +14,12 @@ void main(List<String> arguments) async {
14
14
  help: 'Port for server',
15
15
  defaultsTo: '8080',
16
16
  );
17
+ parser.addOption(
18
+ 'proxy',
19
+ abbr: 'x',
20
+ help: 'Proxy API requests to this URL (e.g., http://localhost:3000)',
21
+ defaultsTo: '',
22
+ );
17
23
 
18
24
  try {
19
25
  final results = parser.parse(arguments);
@@ -24,10 +30,14 @@ void main(List<String> arguments) async {
24
30
  watcher.startWatching(Directory.current.path);
25
31
  } else if (results.command?.name == 'serve') {
26
32
  final port = int.parse(results['port'] as String);
33
+ final proxyUrl = results['proxy'] as String;
27
34
  print('🐸 Starting FroggyDocs server with live reload...');
35
+ if (proxyUrl.isNotEmpty) {
36
+ print('🔄 Proxy enabled: API requests will be forwarded to $proxyUrl');
37
+ }
28
38
 
29
39
  // Start server and watcher
30
- await startServer(port: port);
40
+ await startServer(port: port, proxyUrl: proxyUrl);
31
41
  final watcher = WatcherEngine();
32
42
  watcher.startWatching(Directory.current.path);
33
43
  }
@@ -0,0 +1,251 @@
1
+ let apiData = null;
2
+ let isDark = false;
3
+ let searchQuery = '';
4
+ let authHeader = '';
5
+ const responses = {};
6
+ const loading = {};
7
+ const requestBodyValues = {};
8
+ const expandedEndpoints = {};
9
+
10
+ const methodColors = {
11
+ GET: '#61affe',
12
+ POST: '#49cc90',
13
+ PUT: '#fca130',
14
+ DELETE: '#f93e3e',
15
+ PATCH: '#50e3c2'
16
+ };
17
+
18
+ async function loadData() {
19
+ try {
20
+ const resp = await fetch('/froggy_docs.json');
21
+ if (resp.ok) {
22
+ apiData = await resp.json();
23
+ render();
24
+ }
25
+ } catch (e) {
26
+ console.error('Failed to load API data:', e);
27
+ }
28
+ }
29
+
30
+ function getEndpointsByTag() {
31
+ const byTag = {};
32
+ const paths = apiData?.paths || {};
33
+ for (const [path, methods] of Object.entries(paths)) {
34
+ for (const [method, spec] of Object.entries(methods)) {
35
+ const tags = spec.tags || ['Untagged'];
36
+ for (const tag of tags) {
37
+ if (!byTag[tag]) byTag[tag] = [];
38
+ byTag[tag].push({ path, method, spec });
39
+ }
40
+ }
41
+ }
42
+ return byTag;
43
+ }
44
+
45
+ function render() {
46
+ document.getElementById('docTitle').textContent = apiData?.info?.title || 'API Documentation';
47
+ renderSidebar();
48
+ renderApiList();
49
+ }
50
+
51
+ function renderSidebar() {
52
+ const byTag = getEndpointsByTag();
53
+ const filtered = Object.entries(byTag).filter(([tag, endpoints]) => {
54
+ if (searchQuery === '') return true;
55
+ if (tag.toLowerCase().includes(searchQuery.toLowerCase())) return true;
56
+ return endpoints.some(ep => ep.path.toLowerCase().includes(searchQuery.toLowerCase()));
57
+ });
58
+
59
+ let html = '';
60
+ for (const [tag, endpoints] of filtered) {
61
+ const tagMatchesSearch = searchQuery === '' || tag.toLowerCase().includes(searchQuery.toLowerCase());
62
+ const tagFiltered = searchQuery === '' || tagMatchesSearch
63
+ ? endpoints
64
+ : endpoints.filter(ep => ep.path.toLowerCase().includes(searchQuery.toLowerCase()));
65
+
66
+ if (searchQuery !== '' && tagFiltered.length === 0) continue;
67
+
68
+ html += `
69
+ <div class="tag-group">
70
+ <div class="tag-header">▶ ${tag}</div>
71
+ <div class="tag-endpoints">
72
+ ${tagFiltered.map(ep => `
73
+ <a class="nav-item" href="#${ep.method.toUpperCase()}-${ep.path}">
74
+ <span class="method-tag ${ep.method.toUpperCase()}" style="background:${methodColors[ep.method.toUpperCase()] || '#666'}">${ep.method.toUpperCase()}</span>
75
+ <span>${ep.path}</span>
76
+ </a>
77
+ `).join('')}
78
+ </div>
79
+ </div>
80
+ `;
81
+ }
82
+ document.getElementById('navList').innerHTML = html;
83
+ }
84
+
85
+ function renderApiList() {
86
+ if (!apiData) {
87
+ document.getElementById('apiList').innerHTML = '<div class="api-section"><p>Loading...</p></div>';
88
+ return;
89
+ }
90
+
91
+ const paths = apiData.paths || {};
92
+ let html = '';
93
+ for (const [path, methods] of Object.entries(paths)) {
94
+ for (const [method, spec] of Object.entries(methods)) {
95
+ html += renderEndpoint(path, method, spec);
96
+ }
97
+ }
98
+ document.getElementById('apiList').innerHTML = html;
99
+ }
100
+
101
+ function renderEndpoint(path, method, spec) {
102
+ const key = `${method}-${path}`;
103
+ const props = spec.requestBody?.content?.['application/json']?.schema?.properties;
104
+ const hasAuth = spec.security != null;
105
+ const isExpanded = expandedEndpoints[key] || false;
106
+ const savedValues = requestBodyValues[key] || {};
107
+
108
+ let paramsHtml = '';
109
+ if (props && Object.keys(props).length > 0) {
110
+ paramsHtml = `
111
+ <h3 class="section-title">Parameters</h3>
112
+ <table class="params-table">
113
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
114
+ <tbody>
115
+ ${Object.entries(props).map(([name, prop]) => `
116
+ <tr>
117
+ <td><b>${name}</b></td>
118
+ <td><span class="type-label">${prop.type || 'string'}</span></td>
119
+ <td>${prop.description || '-'}</td>
120
+ </tr>
121
+ `).join('')}
122
+ </tbody>
123
+ </table>
124
+ `;
125
+ }
126
+
127
+ return `
128
+ <section class="api-section" id="${method.toUpperCase()}-${path}">
129
+ <div class="endpoint-header">
130
+ <span class="method-tag ${method.toUpperCase()}" style="background:${methodColors[method.toUpperCase()] || '#666'}">${method.toUpperCase()}</span>
131
+ <span class="path-text">${path}</span>
132
+ ${hasAuth ? '<span class="auth-badge">🔒 Auth</span>' : ''}
133
+ </div>
134
+ <p class="api-description">${spec.summary || 'No description'}</p>
135
+
136
+ ${props && Object.keys(props).length > 0 ? `
137
+ <button class="try-it-out-btn ${isExpanded ? '' : 'secondary'}" onclick="toggleBody('${key}')">
138
+ ${isExpanded ? 'Hide Request Body' : 'Show Request Body'}
139
+ </button>
140
+ ${isExpanded ? `
141
+ <h3 class="section-title">Request Body</h3>
142
+ <div class="request-body-form">
143
+ ${Object.entries(props).map(([name, prop]) => `
144
+ <div class="form-field">
145
+ <label>${name} (${prop.type || 'string'})</label>
146
+ ${prop.description ? `<span class="field-desc">${prop.description}</span>` : ''}
147
+ <input type="text" class="form-input" placeholder="${prop.default || ''}"
148
+ value="${savedValues[name] || ''}"
149
+ oninput="saveBodyValue('${key}', '${name}', this.value)">
150
+ </div>
151
+ `).join('')}
152
+ </div>
153
+ ` : ''}
154
+ ` : ''}
155
+
156
+ <button class="try-it-out-btn" onclick="executeRequest('${method}', '${path}', ${hasAuth}, ${props ? JSON.stringify(props).replace(/"/g, '&quot;') : 'null'})">
157
+ ${loading[key] ? 'Loading...' : 'Try It Out'}
158
+ </button>
159
+
160
+ ${responses[key] ? `
161
+ <div class="response-section">
162
+ <h4>Response:</h4>
163
+ <pre class="response-body">${responses[key]}</pre>
164
+ </div>
165
+ ` : ''}
166
+
167
+ ${paramsHtml}
168
+ </section>
169
+ `;
170
+ }
171
+
172
+ function toggleBody(key) {
173
+ expandedEndpoints[key] = !expandedEndpoints[key];
174
+ renderApiList();
175
+ }
176
+
177
+ function saveBodyValue(key, field, value) {
178
+ if (!requestBodyValues[key]) requestBodyValues[key] = {};
179
+ requestBodyValues[key][field] = value;
180
+ }
181
+
182
+ async function executeRequest(method, path, hasAuth, props) {
183
+ const key = `${method}-${path}`;
184
+ loading[key] = true;
185
+ renderApiList();
186
+
187
+ try {
188
+ const headers = { 'Content-Type': 'application/json' };
189
+ if (authHeader) headers['Authorization'] = authHeader;
190
+
191
+ let body = '{}';
192
+ if (props && Object.keys(props).length > 0) {
193
+ const bodyData = {};
194
+ const savedValues = requestBodyValues[key] || {};
195
+ for (const [name, prop] of Object.entries(props)) {
196
+ const value = savedValues[name] || prop.default || '';
197
+ if (value) bodyData[name] = prop.type === 'number' ? Number(value) : value;
198
+ }
199
+ body = JSON.stringify(bodyData);
200
+ }
201
+
202
+ let resp;
203
+ const url = `${window.location.origin}${path}`;
204
+ const options = { method, headers };
205
+
206
+ if (method !== 'GET' && method !== 'DELETE') {
207
+ options.body = body;
208
+ }
209
+
210
+ resp = await fetch(url, options);
211
+
212
+ let responseText;
213
+ try {
214
+ const json = await resp.json();
215
+ responseText = JSON.stringify(json, null, 2);
216
+ } catch {
217
+ responseText = await resp.text();
218
+ }
219
+
220
+ responses[key] = resp.ok ? responseText : `Error ${resp.status}: ${responseText}`;
221
+ } catch (e) {
222
+ responses[key] = `Error: ${e.message}`;
223
+ }
224
+
225
+ loading[key] = false;
226
+ renderApiList();
227
+ }
228
+
229
+ document.getElementById('searchInput').addEventListener('input', (e) => {
230
+ searchQuery = e.target.value;
231
+ render();
232
+ });
233
+
234
+ document.getElementById('authInput').addEventListener('input', (e) => {
235
+ authHeader = e.target.value;
236
+ });
237
+
238
+ document.getElementById('themeToggle').addEventListener('click', () => {
239
+ isDark = !isDark;
240
+ document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
241
+ document.getElementById('themeToggle').textContent = isDark ? '☀️' : '🌙';
242
+ });
243
+
244
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
245
+ isDark = true;
246
+ document.documentElement.setAttribute('data-theme', 'dark');
247
+ document.getElementById('themeToggle').textContent = '☀️';
248
+ }
249
+
250
+ loadData();
251
+ setInterval(loadData, 3000);
@@ -1,12 +1,34 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>FroggyDocs</title>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FroggyDocs - API Documentation</title>
7
7
  <link rel="stylesheet" href="styles.css">
8
8
  </head>
9
9
  <body>
10
- <script defer src="main.client.dart.js"></script>
10
+ <div class="app-container">
11
+ <aside class="sidebar">
12
+ <div class="sidebar-header">
13
+ <h2><span>🐸</span> FroggyDocs</h2>
14
+ </div>
15
+ <div class="search-container">
16
+ <input type="text" class="search-box" id="searchInput" placeholder="Search tags or endpoints...">
17
+ </div>
18
+ <nav class="nav-list" id="navList"></nav>
19
+ </aside>
20
+ <main class="main-content">
21
+ <header class="header">
22
+ <h1 id="docTitle">API Documentation</h1>
23
+ <button class="theme-toggle" id="themeToggle">🌙</button>
24
+ </header>
25
+ <div class="settings-section">
26
+ <div>Authorization: </div>
27
+ <input type="text" class="auth-input" id="authInput" placeholder="Bearer token">
28
+ </div>
29
+ <div id="apiList"></div>
30
+ </main>
31
+ </div>
32
+ <script src="app.js"></script>
11
33
  </body>
12
34
  </html>
@@ -5,10 +5,15 @@ import 'package:shelf/shelf.dart';
5
5
  import 'package:shelf/shelf_io.dart' as shelf_io;
6
6
  import 'package:shelf_router/shelf_router.dart';
7
7
  import 'package:path/path.dart' as p;
8
+ import 'package:http/http.dart' as http;
8
9
 
9
10
  const defaultPort = 8080;
10
11
 
11
- Future<void> startServer({int port = defaultPort}) async {
12
+ String _proxyUrl = '';
13
+
14
+ Future<void> startServer({int port = defaultPort, String proxyUrl = ''}) async {
15
+ _proxyUrl = proxyUrl;
16
+
12
17
  final handler = const Pipeline()
13
18
  .addMiddleware(logRequests())
14
19
  .addHandler(_router.call);
@@ -137,8 +142,51 @@ Router get _router {
137
142
  // ═════════════════════════════════════════════════════════════
138
143
  // End of Demo/Mock API
139
144
  // In production: Remove lines 66-124 or connect to your real API
140
- // The "Try It Out" button will call your actual API endpoints
141
- // ═════════════════════════════════════════════════════════════
145
+ // The "Try It Out" button will call your actual API endpoints
146
+ // ═══════════════════════════════════════════════════════════════
147
+
148
+ // Proxy API requests to backend server
149
+ router.all('/api/<path.*>', (Request request, String path) async {
150
+ if (_proxyUrl.isEmpty) {
151
+ return Response.notFound('{"error": "Proxy not configured. Use --proxy http://localhost:3000"');
152
+ }
153
+
154
+ try {
155
+ final targetUrl = '$_proxyUrl/api/$path';
156
+ final method = request.method;
157
+ final headers = <String, String>{};
158
+
159
+ request.headers.forEach((key, value) {
160
+ if (key.toLowerCase() != 'host') {
161
+ headers[key] = value;
162
+ }
163
+ });
164
+
165
+ String? body;
166
+ if (method != 'GET' && method != 'HEAD') {
167
+ body = await request.readAsString();
168
+ }
169
+
170
+ final proxyResponse = await http.Request(method, Uri.parse(targetUrl))
171
+ .send();
172
+
173
+ final responseBody = await proxyResponse.stream.bytesToString();
174
+
175
+ return Response(
176
+ proxyResponse.statusCode,
177
+ body: responseBody,
178
+ headers: {
179
+ 'Content-Type': proxyResponse.headers['content-type'] ?? 'application/json',
180
+ ..._corsHeaders,
181
+ },
182
+ );
183
+ } catch (e) {
184
+ return Response.internalServerError(
185
+ body: '{"error": "Proxy error: $e"}',
186
+ headers: {'Content-Type': 'application/json'},
187
+ );
188
+ }
189
+ });
142
190
 
143
191
  router.get('/<path|[^/]+>', (Request request, String path) async {
144
192
  var file = File(p.join(deployDir, path));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "froggy-docs",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "Auto-generate API documentation from code annotations. Works with any programming language.",
5
5
  "author": "Kaung Mrat Thu <kaungmyatthuu.dev@gmail.com>",
6
6
  "homepage": "https://github.com/Kaung-Myat/froggydocs",
package/pubspec.yaml CHANGED
@@ -19,6 +19,7 @@ dependencies:
19
19
  watcher: ^1.2.1
20
20
  shelf: ^1.4.2
21
21
  shelf_router: ^1.1.4
22
+ http: ^1.4.0
22
23
 
23
24
  dev_dependencies:
24
25
  lints: ^6.0.0
@@ -1,81 +0,0 @@
1
- (function() {
2
- var _currentDirectory = (function () {
3
- var _url;
4
- var lines = new Error().stack.split('\n');
5
- function lookupUrl() {
6
- if (lines.length > 2) {
7
- var match = lines[1].match(/^\s+at (.+):\d+:\d+$/);
8
- // Chrome.
9
- if (match) return match[1];
10
- // Chrome nested eval case.
11
- match = lines[1].match(/^\s+at eval [(](.+):\d+:\d+[)]$/);
12
- if (match) return match[1];
13
- // Edge.
14
- match = lines[1].match(/^\s+at.+\((.+):\d+:\d+\)$/);
15
- if (match) return match[1];
16
- // Firefox.
17
- match = lines[0].match(/[<][@](.+):\d+:\d+$/)
18
- if (match) return match[1];
19
- }
20
- // Safari.
21
- return lines[0].match(/[@](.+):\d+:\d+$/)[1];
22
- }
23
- _url = lookupUrl();
24
- var lastSlash = _url.lastIndexOf('/');
25
- if (lastSlash == -1) return _url;
26
- var currentDirectory = _url.substring(0, lastSlash + 1);
27
- return currentDirectory;
28
- })();
29
-
30
- var baseUrl = (function () {
31
- // Attempt to detect --precompiled mode for tests, and set the base url
32
- // appropriately, otherwise set it to '/'.
33
- var pathParts = location.pathname.split("/");
34
- if (pathParts[0] == "") {
35
- pathParts.shift();
36
- }
37
- if (pathParts.length > 1 && pathParts[1] == "test") {
38
- return "/" + pathParts.slice(0, 2).join("/") + "/";
39
- }
40
- // Attempt to detect base url using <base href> html tag
41
- // base href should start and end with "/"
42
- if (typeof document !== 'undefined') {
43
- var el = document.getElementsByTagName('base');
44
- if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute
45
- ("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){
46
- return el[0].getAttribute("href");
47
- }
48
- }
49
- // return default value
50
- return "/";
51
- }());
52
-
53
-
54
- var mapperUri = baseUrl + "packages/build_web_compilers/src/dev_compiler_stack_trace/stack_trace_mapper.dart.js";
55
- var requireUri = baseUrl +
56
- "packages/build_web_compilers/src/dev_compiler/require.js";
57
- var mainUri = _currentDirectory + "main.client.dart.bootstrap";
58
-
59
- if (typeof document != 'undefined') {
60
- var el = document.createElement("script");
61
- el.defer = true;
62
- el.async = false;
63
- el.src = mapperUri;
64
- document.head.appendChild(el);
65
-
66
- el = document.createElement("script");
67
- el.defer = true;
68
- el.async = false;
69
- el.src = requireUri;
70
- el.setAttribute("data-main", mainUri);
71
- document.head.appendChild(el);
72
- } else {
73
- importScripts(mapperUri, requireUri);
74
- require.config({
75
- baseUrl: baseUrl,
76
- });
77
- // TODO: update bootstrap code to take argument - dart-lang/build#1115
78
- window = self;
79
- require([mainUri + '.js']);
80
- }
81
- })();