@aimeloic/monkey-tester 1.0.0 → 1.0.2

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 +40 -45
  2. package/index.js +49 -64
  3. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -112,17 +112,14 @@ export function getHtmlTemplate(endpoints) {
112
112
  <div id="toast"></div>
113
113
 
114
114
  <script>
115
- // The backend server automatically stringifies the runtime mapping straight into here!
116
- const ENDPOINTS = ${JSON.stringify(endpoints, null, 2)};
115
+ // Safely parse the object passed from node environment to prevent script token breaking
116
+ const ENDPOINTS = ${JSON.stringify(endpoints)};
117
117
 
118
118
  let currentEp = '';
119
119
 
120
120
  // Setup host variables contextually on layout initialization
121
121
  document.getElementById('base-url').value = window.location.origin;
122
122
 
123
- function getBase() { return document.getElementById('base-url').value.replace(/\/$/, ''); }
124
- function getJwt() { return document.getElementById('jwt-input').value.trim(); }
125
-
126
123
  function buildSidebar() {
127
124
  const sidebar = document.getElementById('sidebar-nav');
128
125
  const keys = Object.keys(ENDPOINTS);
@@ -135,9 +132,9 @@ function buildSidebar() {
135
132
  keys.forEach((key, index) => {
136
133
  const ep = ENDPOINTS[key];
137
134
  const div = document.createElement('div');
138
- div.className = \`nav-item \${index === 0 ? 'active' : ''}\`;
135
+ div.className = index === 0 ? 'nav-item active' : 'nav-item';
139
136
  div.setAttribute('data-ep', key);
140
- div.innerHTML = \`<span class="method-badge \${ep.method}">\${ep.method}</span><span class="nav-label">\${ep.path}</span>\`;
137
+ div.innerHTML = '<span class="method-badge ' + ep.method + '">' + ep.method + '</span><span class="nav-label">' + ep.path + '</span>';
141
138
 
142
139
  div.addEventListener('click', () => {
143
140
  document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
@@ -159,41 +156,39 @@ function renderPanel(epKey) {
159
156
  const main = document.getElementById('main-panel');
160
157
  if(!ep) return;
161
158
 
162
- let html = \`
163
- <div class="endpoint-title">\${ep.title}</div>
164
- <div class="endpoint-path">
165
- <span class="method-badge \${ep.method}">\${ep.method}</span>
166
- <span>\${ep.path}</span>
167
- </div>
168
- <div class="endpoint-desc">\${ep.desc}</div>
169
- \`;
159
+ let html = '<div class="endpoint-title">' + ep.title + '</div>' +
160
+ '<div class="endpoint-path">' +
161
+ '<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
162
+ '<span>' + ep.path + '</span>' +
163
+ '</div>' +
164
+ '<div class="endpoint-desc">' + ep.desc + '</div>';
170
165
 
171
- if (ep.params?.length) {
172
- html += \`<div class="form-section"><div class="form-section-title">Path Parameters</div>\`;
166
+ if (ep.params && ep.params.length) {
167
+ html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
173
168
  ep.params.forEach(p => {
174
- html += \`<div class="field-row">
175
- <label class="field-label">\${p.label}</label>
176
- <input type="text" id="param-\${p.name}" placeholder="\${p.placeholder}" />
177
- </div>\`;
169
+ html += '<div class="field-row">' +
170
+ '<label class="field-label">' + p.label + '</label>' +
171
+ '<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" />' +
172
+ '</div>';
178
173
  });
179
- html += \`</div>\`;
174
+ html += '</div>';
180
175
  }
181
176
 
182
- if (ep.fields?.length) {
183
- html += \`<div class="form-section"><div class="form-section-title">JSON Request Body Raw Payload</div>\`;
177
+ if (ep.fields && ep.fields.length) {
178
+ html += '<div class="form-section"><div class="form-section-title">JSON Request Body Raw Payload</div>';
184
179
  ep.fields.forEach(f => {
185
- html += \`<div class="field-row">
186
- <label class="field-label">\${f.label}</label>
187
- <input type="text" id="field-\${f.name}" value='{"key": "value"}' />
188
- </div>\`;
180
+ html += '<div class="field-row">' +
181
+ '<label class="field-label">' + f.label + '</label>' +
182
+ '<input type="text" id="field-' + f.name + '" value=\'{"key": "value"}\' />' +
183
+ '</div>';
189
184
  });
190
- html += \`</div>\`;
185
+ html += '</div>';
191
186
  }
192
187
 
193
- html += \`<div class="btn-row">
194
- <button class="btn" onclick="sendRequest()">Execute Route</button>
195
- <button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>
196
- </div>\`;
188
+ html += '<div class="btn-row">' +
189
+ '<button class="btn" onclick="sendRequest()">Execute Route</button>' +
190
+ '<button class="btn btn-secondary" onclick="clearResponse()">Clear Context</button>' +
191
+ '</div>';
197
192
 
198
193
  main.innerHTML = html;
199
194
  }
@@ -204,9 +199,9 @@ async function sendRequest() {
204
199
 
205
200
  if (ep.params) {
206
201
  for (const p of ep.params) {
207
- const val = document.getElementById(\`param-\${p.name}\`)?.value.trim();
208
- if (!val) { showToast(\`⚠ Parameter \${p.label} required\`); return; }
209
- path = path.replace(\`:\${p.name}\`, encodeURIComponent(val));
202
+ const val = document.getElementById('param-' + p.name)?.value.trim();
203
+ if (!val) { showToast('⚠ Parameter ' + p.label + ' required'); return; }
204
+ path = path.replace(':' + p.name, encodeURIComponent(val));
210
205
  }
211
206
  }
212
207
 
@@ -214,11 +209,11 @@ async function sendRequest() {
214
209
  const headers = { 'Content-Type': 'application/json' };
215
210
 
216
211
  const jwt = getJwt();
217
- if (jwt) headers['Authorization'] = \`Bearer \${jwt}\`;
212
+ if (jwt) headers['Authorization'] = 'Bearer ' + jwt;
218
213
 
219
214
  let body = undefined;
220
- if (ep.fields?.length && ['POST','PUT','PATCH'].includes(ep.method)) {
221
- const rawVal = document.getElementById(\`field-\${ep.fields[0].name}\`).value.trim();
215
+ if (ep.fields && ep.fields.length && ['POST','PUT','PATCH'].includes(ep.method)) {
216
+ const rawVal = document.getElementById('field-' + ep.fields[0].name).value.trim();
222
217
  try {
223
218
  JSON.parse(rawVal);
224
219
  body = rawVal;
@@ -254,8 +249,8 @@ function setResponse(data, state, status, ms) {
254
249
  return;
255
250
  }
256
251
 
257
- badge.className = \`status-badge \${state === 'ok' ? 'status-ok' : 'status-err'}\`;
258
- badge.textContent = \`\${status} · \${ms}ms\`;
252
+ badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
253
+ badge.textContent = status + ' · ' + ms + 'ms';
259
254
  body.innerHTML = highlightJson(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
260
255
  }
261
256
 
@@ -273,10 +268,10 @@ function highlightJson(str) {
273
268
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
274
269
  .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, match => {
275
270
  if (/^"/.test(match)) {
276
- if (/:$/.test(match)) return \`<span class="json-key">\${match}</span>\`;
277
- return \`<span class="json-str">\${match}</span>\`;
271
+ if (/:$/.test(match)) return '<span class="json-key">' + match + '</span>';
272
+ return '<span class="json-str">' + match + '</span>';
278
273
  }
279
- return \`<span class="json-num">\${match}</span>\`;
274
+ return '<span class="json-num">' + match + '</span>';
280
275
  });
281
276
  }
282
277
 
@@ -286,7 +281,7 @@ function showToast(msg) {
286
281
  setTimeout(() => t.classList.remove('show'), 2500);
287
282
  }
288
283
 
289
- // Initial fire
284
+ // Initial script activation
290
285
  buildSidebar();
291
286
  </script>
292
287
  </body>
package/index.js CHANGED
@@ -1,83 +1,68 @@
1
1
  import { getHtmlTemplate } from './htmlTemplate.js';
2
2
 
3
- /**
4
- * Express middleware to automatically map routes and serve the endtester UI
5
- */
6
3
  export function endtesterExpress() {
7
4
  return (req, res, next) => {
8
- // We only intercept requests hitting the tester path
9
5
  if (req.path !== '/api/tester' && req.path !== '/api/tester/') {
10
6
  return next();
11
7
  }
12
8
 
13
- // 1. Inspect the Express app to extract all registered routes
14
9
  const expressApp = req.app;
15
- const routesStack = expressApp._router.stack;
16
10
  const detectedEndpoints = {};
17
11
 
18
- routesStack.forEach((middleware) => {
19
- if (middleware.route) {
20
- // Simple top-level routes
21
- processRoute(middleware.route, detectedEndpoints);
22
- } else if (middleware.name === 'router') {
23
- // Nested router stacks (e.g., app.use('/api', apiRouter))
24
- const baseUrl = middleware.regexp.source
25
- .replace('^\\', '')
26
- .replace('\\/?(?=\\/|$)', '')
27
- .replace('\\', '');
12
+ // Recursive function to dig through all layers of Express routes
13
+ function parseStack(stack, prefix = '') {
14
+ if (!stack) return;
15
+
16
+ stack.forEach((layer) => {
17
+ if (layer.route) {
18
+ // It's a direct route (e.g., app.get('/path'))
19
+ const methods = Object.keys(layer.route.methods);
20
+ const path = (prefix + layer.route.path).replace(/\/+/g, '/');
28
21
 
29
- middleware.handle.stack.forEach((nestedHandler) => {
30
- if (nestedHandler.route) {
31
- processRoute(nestedHandler.route, detectedEndpoints, baseUrl);
22
+ if (path.includes('/api/tester')) return; // Skip ourselves
23
+
24
+ methods.forEach((method) => {
25
+ const httpMethod = method.toUpperCase();
26
+ const key = `${httpMethod.toLowerCase()}-${path.replace(/[^a-zA-Z0-9]/g, '-')}`;
27
+
28
+ const pathParams = layer.route.keys ? layer.route.keys.map(k => ({
29
+ name: k.name,
30
+ label: k.name.toUpperCase(),
31
+ placeholder: 'value'
32
+ })) : [];
33
+
34
+ detectedEndpoints[key] = {
35
+ method: httpMethod,
36
+ path: path,
37
+ auth: false,
38
+ title: `${httpMethod} ${path}`,
39
+ desc: `Auto-discovered endpoint: ${path}`,
40
+ params: pathParams,
41
+ fields: ['POST', 'PUT', 'PATCH'].includes(httpMethod) ? [{ name: 'payload', label: 'JSON Body', type: 'text' }] : []
42
+ };
43
+ });
44
+ } else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
45
+ // It's a router middleware (e.g., app.use('/api', myRouter))
46
+ let routerPath = '';
47
+ if (layer.regexp) {
48
+ // Extract the base path string from the router regex safely
49
+ const match = layer.regexp.toString().match(/^\/\^\\(.*?)\\\/\?/);
50
+ if (match && match[1]) {
51
+ routerPath = match[1].replace(/\\/g, '');
52
+ }
32
53
  }
33
- });
34
- }
35
- });
54
+ parseStack(layer.handle.stack, prefix + '/' + routerPath);
55
+ }
56
+ });
57
+ }
36
58
 
37
- // 2. Generate the HTML template injecting our found endpoints
38
- const fullHtml = getHtmlTemplate(detectedEndpoints);
59
+ // Fire the scanner on the main app router stack
60
+ if (expressApp._router && expressApp._router.stack) {
61
+ parseStack(expressApp._router.stack);
62
+ }
39
63
 
40
- // 3. Serve the interactive tester page
64
+ const fullHtml = getHtmlTemplate(detectedEndpoints);
41
65
  res.setHeader('Content-Type', 'text/html');
42
66
  return res.send(fullHtml);
43
67
  };
44
68
  }
45
-
46
- /**
47
- * Formats Express route data into the shape your UI script expects
48
- */
49
- function processRoute(route, targetObject, prefix = '') {
50
- const fullPath = `${prefix}${route.path}`.replace(/\/+/g, '/');
51
-
52
- // Skip the tester endpoint itself to avoid recursion confusion
53
- if (fullPath.includes('/api/tester')) return;
54
-
55
- // Extract path parameters (e.g., :id)
56
- const pathParams = route.keys ? route.keys.map(k => ({
57
- name: k.name,
58
- label: k.name.toUpperCase(),
59
- placeholder: 'value'
60
- })) : [];
61
-
62
- Object.keys(route.methods).forEach((method) => {
63
- if (!route.methods[method]) return;
64
-
65
- const httpMethod = method.toUpperCase();
66
- // Unique key for your UI state
67
- const key = `${httpMethod.toLowerCase()}-${fullPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
68
-
69
- // Infer basic structure based on request types
70
- const needsBody = ['POST', 'PUT', 'PATCH'].includes(httpMethod);
71
-
72
- targetObject[key] = {
73
- method: httpMethod,
74
- path: fullPath,
75
- auth: true, // Default to true, users can toggle or override
76
- title: `${httpMethod} ${fullPath}`,
77
- desc: `Auto-generated tester for ${fullPath}`,
78
- params: pathParams,
79
- queryParams: httpMethod === 'GET' ? [{ name: 'limit', label: 'Limit', placeholder: '10' }] : [],
80
- fields: needsBody ? [{ name: 'payload', label: 'JSON Body Field', type: 'text', placeholder: 'Data string' }] : []
81
- };
82
- });
83
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Embedded interactive API testing UI for Node.js backends",
5
5
  "main": "index.js",
6
6
  "type":"module",