@aimeloic/monkey-tester 3.0.4 โ†’ 3.0.6

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.
Files changed (3) hide show
  1. package/htmlTemplate.js +12 -12
  2. package/monkey.js +33 -26
  3. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -8,7 +8,7 @@ function getHtmlTemplate(endpoints) {
8
8
  <head>
9
9
  <meta charset="UTF-8">
10
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
- <title>Monkey Tester โ€” API Sandbox</title>
11
+ <title>Monkey Tester &mdash; API Sandbox</title>
12
12
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
13
13
  <style>
14
14
  :root {
@@ -153,7 +153,7 @@ function getHtmlTemplate(endpoints) {
153
153
  <div id="__monkey_data__" data-payload="${safeJsonString}" style="display:none;"></div>
154
154
 
155
155
  <header>
156
- <div class="logo">๐Ÿ’ Monkey Tester <span>API Sandbox</span></div>
156
+ <div class="logo">&#x1F412; Monkey Tester <span>API Sandbox</span></div>
157
157
  <div class="header-right">
158
158
  <div class="base-url-wrap">
159
159
  <label>TARGET HOST</label>
@@ -174,7 +174,7 @@ function getHtmlTemplate(endpoints) {
174
174
  <div class="response-panel">
175
175
  <div class="response-header">
176
176
  <span class="response-header-title">Response</span>
177
- <span id="status-badge" class="status-badge status-idle">โ€”</span>
177
+ <span id="status-badge" class="status-badge status-idle">&mdash;</span>
178
178
  </div>
179
179
  <div class="response-body empty" id="response-body">Execute a request to see the response</div>
180
180
  </div>
@@ -265,7 +265,7 @@ function getHtmlTemplate(endpoints) {
265
265
 
266
266
  function showEmptyState() {
267
267
  document.getElementById('main-panel').innerHTML =
268
- '<div class="empty-state"><div class="monkey">๐Ÿ’</div>' +
268
+ '<div class="empty-state"><div class="monkey">&#x1F412;</div>' +
269
269
  '<h2>No endpoints found</h2>' +
270
270
  '<p>Make sure <code>app.use(endtesterExpress())</code> is added after your routes.</p></div>';
271
271
  }
@@ -278,7 +278,7 @@ function getHtmlTemplate(endpoints) {
278
278
  for (var i = 0; i < ep.params.length; i++) {
279
279
  var p = ep.params[i];
280
280
  var val = (document.getElementById('param-' + p.name) || {}).value || '';
281
- if (!val.trim()) { showToast('โš  Path param "' + p.label + '" is required'); return; }
281
+ if (!val.trim()) { showToast('[!] Path param "' + p.label + '" is required'); return; }
282
282
  path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
283
283
  }
284
284
  }
@@ -293,18 +293,18 @@ function getHtmlTemplate(endpoints) {
293
293
  if (['POST', 'PUT', 'PATCH'].includes(ep.method) && ep.fields && ep.fields.length) {
294
294
  var payload = {};
295
295
  var hasData = false;
296
-
296
+
297
297
  ep.fields.forEach(function(f) {
298
298
  var el = document.getElementById('field-' + f.name);
299
299
  if (!el) return;
300
300
  var v = el.value.trim();
301
301
  if (v === '') return;
302
-
302
+
303
303
  if (f.type === 'number') v = Number(v);
304
304
  payload[f.name] = v;
305
305
  hasData = true;
306
306
  });
307
-
307
+
308
308
  if (hasData) body = JSON.stringify(payload);
309
309
  }
310
310
 
@@ -329,14 +329,14 @@ function getHtmlTemplate(endpoints) {
329
329
 
330
330
  if (state === 'loading') {
331
331
  badge.className = 'status-badge status-idle';
332
- badge.textContent = 'โ€ฆ';
332
+ badge.textContent = '...';
333
333
  body.className = 'response-body empty';
334
- body.textContent = 'Sendingโ€ฆ';
334
+ body.textContent = 'Sending...';
335
335
  return;
336
336
  }
337
337
 
338
338
  badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
339
- badge.textContent = status + ' ยท ' + ms + 'ms';
339
+ badge.textContent = status + ' \u00b7 ' + ms + 'ms';
340
340
  body.className = 'response-body';
341
341
  var str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
342
342
  body.innerHTML = '<pre class="json-render-block">' + highlight(str) + '</pre>';
@@ -344,7 +344,7 @@ function getHtmlTemplate(endpoints) {
344
344
 
345
345
  function clearResponse() {
346
346
  document.getElementById('status-badge').className = 'status-badge status-idle';
347
- document.getElementById('status-badge').textContent = 'โ€”';
347
+ document.getElementById('status-badge').textContent = '\u2014';
348
348
  var body = document.getElementById('response-body');
349
349
  body.className = 'response-body empty';
350
350
  body.textContent = 'Execute a request to see the response';
package/monkey.js CHANGED
@@ -85,20 +85,27 @@ function fallbackFields(path) {
85
85
 
86
86
  // โ”€โ”€โ”€ Extract router prefix safely from Express layer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
87
  function extractRouterPrefix(layer) {
88
+ // Prefer explicit path if available
89
+ if (layer.path && typeof layer.path === 'string') {
90
+ return layer.path === '/' ? '' : layer.path;
91
+ }
92
+
88
93
  if (!layer.regexp) return '';
89
- if (layer.path) return layer.path; // If explicit paths exist
90
-
91
- let src = layer.regexp.source;
92
-
93
- // Strip out boilerplate generated patterns from default express mounting structures
94
- src = src
95
- .replace('^\\/', '/')
96
- .replace('\\/?(?=\\/|$)', '')
97
- .replace('(?:\\/(?=$))?(?=\\/|$)', '')
98
- .replace('\\/', '/');
99
-
100
- const cleanPath = src.split('(?')[0].replace(/\\/g, '');
101
- return cleanPath === '/' ? '' : cleanPath;
94
+
95
+ // Convert the regexp back to a path prefix by looking at the regexp source
96
+ // Express generates regexps like: /^\/api\/v1\/?(?=\/|$)/i
97
+ const src = layer.regexp.source;
98
+
99
+ // Extract the literal path segment before any optional/lookahead parts
100
+ // Match from start: ^\/ then literal segments
101
+ const match = src.match(/^\^((?:\\\/[^\\(?[*+{}|$^]+)+)/);
102
+ if (!match) return '';
103
+
104
+ // Unescape the extracted path
105
+ const raw = match[1].replace(/\\\//g, '/');
106
+
107
+ // Remove trailing slash if present
108
+ return raw.replace(/\/$/, '') || '';
102
109
  }
103
110
 
104
111
  // โ”€โ”€โ”€ Walk the Express router stack recursively โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -114,6 +121,7 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
114
121
 
115
122
  const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
116
123
 
124
+ // Skip the tester route itself
117
125
  if (fullPath.startsWith('/api/tester')) continue;
118
126
 
119
127
  const methods = Object.keys(layer.route.methods || {});
@@ -122,7 +130,7 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
122
130
  const httpMethod = method.toUpperCase();
123
131
  const key = `${httpMethod}::${fullPath}`;
124
132
 
125
- // โ”€โ”€ Path params (:id, :slug โ€ฆ) safely parsed using matchAll โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
133
+ // โ”€โ”€ Path params (:id, :slug โ€ฆ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
126
134
  const pathParams = [];
127
135
  const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
128
136
  const matches = [...fullPath.matchAll(paramRe)];
@@ -135,13 +143,14 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
135
143
  });
136
144
  }
137
145
 
138
- // โ”€โ”€ Body fields โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
146
+ // โ”€โ”€ Body fields โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
139
147
  let bodyFields = [];
140
148
  if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
141
149
  const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
142
150
  for (const handler of handlers) {
143
151
  bodyFields.push(...extractBodyFields(handler));
144
152
  }
153
+ // Deduplicate
145
154
  const seen = new Map();
146
155
  bodyFields = bodyFields.filter(f => {
147
156
  if (seen.has(f.name)) return false;
@@ -165,7 +174,7 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
165
174
  }
166
175
 
167
176
  // โ”€โ”€ Nested router (app.use('/prefix', router)) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
168
- else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
177
+ else if (layer.handle && typeof layer.handle === 'function' && layer.handle.stack) {
169
178
  const routerPrefix = extractRouterPrefix(layer);
170
179
  parseStack(layer.handle.stack, detectedEndpoints, prefix + routerPrefix);
171
180
  }
@@ -173,19 +182,19 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
173
182
  }
174
183
 
175
184
  // โ”€โ”€โ”€ Middleware โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
176
- // โ”€โ”€โ”€ Middleware in monkey.js โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
177
185
  function endtesterExpress() {
178
186
  return function monkeyTesterMiddleware(req, res, next) {
179
- // Standard structural path cleaning
180
- const normalPath = req.path.replace(/\/$/, ''); // removes trailing slash
181
-
182
- if (normalPath !== '/api/tester') {
187
+ // Normalize path: strip trailing slash, handle both req.path and req.url
188
+ const rawPath = (req.path || req.url || '').split('?')[0].replace(/\/+$/, '');
189
+
190
+ if (rawPath !== '/api/tester') {
183
191
  return next();
184
192
  }
185
193
 
186
194
  const app = req.app;
187
-
188
- // CRITICAL FIX: Reset the detected endpoints on each call to prevent stale empty states
195
+
196
+ // Wait a tick to ensure all routes are registered before scanning
197
+ // (handles edge cases where middleware is mounted before some routes)
189
198
  const detectedEndpoints = {};
190
199
 
191
200
  const rootStack =
@@ -193,12 +202,10 @@ function endtesterExpress() {
193
202
  (app.router && app.router.stack) || // Express 5
194
203
  [];
195
204
 
196
- // Scan the live compiled routing stack
197
205
  parseStack(rootStack, detectedEndpoints);
198
206
 
199
- // Render page template
200
207
  const html = getHtmlTemplate(detectedEndpoints);
201
- res.setHeader('Content-Type', 'text/html');
208
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
202
209
  return res.send(html);
203
210
  };
204
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",