zerra-core 1.2.0 → 1.2.1
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/index.d.ts +47 -0
- package/index.js +308 -49
- package/package.json +3 -2
package/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
|
|
3
|
+
export interface ZerraRequest extends IncomingMessage {
|
|
4
|
+
query: Record<string, string>;
|
|
5
|
+
path: string;
|
|
6
|
+
body: any;
|
|
7
|
+
files: Array<{
|
|
8
|
+
fieldname: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
encoding: string;
|
|
11
|
+
mimetype: string;
|
|
12
|
+
buffer: Buffer;
|
|
13
|
+
}>;
|
|
14
|
+
params: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ZerraResponse extends ServerResponse {
|
|
18
|
+
status(code: number): ZerraResponse;
|
|
19
|
+
json(data: any): void;
|
|
20
|
+
cors(options?: { origin?: string; methods?: string }): ZerraResponse;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ZerraHandler = (req: ZerraRequest, res: ZerraResponse) => void | Promise<void>;
|
|
24
|
+
|
|
25
|
+
export type ZerraMiddleware = (req: ZerraRequest, res: ZerraResponse, next: () => Promise<void>) => void | Promise<void>;
|
|
26
|
+
|
|
27
|
+
export interface ZerraConfig {
|
|
28
|
+
features: {
|
|
29
|
+
logging?: boolean;
|
|
30
|
+
dynamicRouting?: boolean;
|
|
31
|
+
middleware?: boolean;
|
|
32
|
+
dotenv?: boolean;
|
|
33
|
+
validation?: boolean;
|
|
34
|
+
multipart?: boolean;
|
|
35
|
+
errors?: boolean;
|
|
36
|
+
dashboard?: boolean;
|
|
37
|
+
};
|
|
38
|
+
plugins?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ZerraApp {
|
|
42
|
+
use(fn: ZerraMiddleware): void;
|
|
43
|
+
decorate(target: 'req' | 'res', name: string, fn: Function): void;
|
|
44
|
+
config: ZerraConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function startServer(port?: number): void;
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require("fs");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
|
|
5
5
|
function startServer(port = 3000) {
|
|
6
|
+
const jiti = require("jiti")(__filename);
|
|
6
7
|
const configPath = path.join(process.cwd(), 'zerra.config.json');
|
|
7
8
|
let config = {
|
|
8
9
|
features: {
|
|
@@ -26,6 +27,10 @@ function startServer(port = 3000) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
const customEnvKeys = new Set();
|
|
31
|
+
const recentRequests = [];
|
|
32
|
+
const MAX_LOGS = 20;
|
|
33
|
+
|
|
29
34
|
// 8. Enhanced DX: Auto-load .env files
|
|
30
35
|
if (config.features.dotenv) {
|
|
31
36
|
const envPath = path.join(process.cwd(), '.env');
|
|
@@ -39,7 +44,11 @@ function startServer(port = 3000) {
|
|
|
39
44
|
// Remove quotes
|
|
40
45
|
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
41
46
|
else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
42
|
-
|
|
47
|
+
|
|
48
|
+
if (!process.env.hasOwnProperty(key)) {
|
|
49
|
+
process.env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
customEnvKeys.add(key);
|
|
43
52
|
}
|
|
44
53
|
});
|
|
45
54
|
}
|
|
@@ -62,7 +71,7 @@ function startServer(port = 3000) {
|
|
|
62
71
|
if (config.plugins && Array.isArray(config.plugins)) {
|
|
63
72
|
config.plugins.forEach(pluginPath => {
|
|
64
73
|
try {
|
|
65
|
-
const plugin =
|
|
74
|
+
const plugin = jiti(path.isAbsolute(pluginPath) ? pluginPath : path.join(process.cwd(), pluginPath));
|
|
66
75
|
if (typeof plugin === 'function') plugin(zerra);
|
|
67
76
|
} catch (e) {
|
|
68
77
|
console.error(`❌ Failed to load plugin: ${pluginPath}`, e);
|
|
@@ -79,12 +88,28 @@ function startServer(port = 3000) {
|
|
|
79
88
|
// 1. Enhanced DX: Beautiful Request Logging
|
|
80
89
|
const originalEnd = res.end;
|
|
81
90
|
res.end = function (...args) {
|
|
91
|
+
const duration = Date.now() - startTime;
|
|
92
|
+
const path = req.path || url;
|
|
93
|
+
|
|
94
|
+
// Log to terminal
|
|
82
95
|
if (config.features.logging) {
|
|
83
|
-
const duration = Date.now() - startTime;
|
|
84
96
|
const statusColor = res.statusCode >= 500 ? '\x1b[31m' : res.statusCode >= 400 ? '\x1b[33m' : '\x1b[32m';
|
|
85
97
|
const resetColor = '\x1b[0m';
|
|
86
|
-
console.log(`${statusColor}[${method}]${resetColor} ${
|
|
98
|
+
console.log(`${statusColor}[${method}]${resetColor} ${path} ➜ ${statusColor}${res.statusCode}${resetColor} (${duration}ms)`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Store for dashboard (exclude the dashboard itself)
|
|
102
|
+
if (path !== '/__zerra' && path !== '/favicon.ico') {
|
|
103
|
+
recentRequests.unshift({
|
|
104
|
+
method,
|
|
105
|
+
path,
|
|
106
|
+
statusCode: res.statusCode,
|
|
107
|
+
duration,
|
|
108
|
+
timestamp: new Date().toLocaleTimeString()
|
|
109
|
+
});
|
|
110
|
+
if (recentRequests.length > MAX_LOGS) recentRequests.pop();
|
|
87
111
|
}
|
|
112
|
+
|
|
88
113
|
return originalEnd.apply(this, args);
|
|
89
114
|
};
|
|
90
115
|
|
|
@@ -191,9 +216,18 @@ function startServer(port = 3000) {
|
|
|
191
216
|
const stat = fs.statSync(filePath);
|
|
192
217
|
if (stat && stat.isDirectory()) {
|
|
193
218
|
results = results.concat(getRoutes(filePath, path.join(base, file)));
|
|
194
|
-
} else if (file.endsWith('.js') && !file.startsWith('_')) {
|
|
195
|
-
const route = path.join(base, file).replace(/\\/g, '/').replace(
|
|
196
|
-
|
|
219
|
+
} else if ((file.endsWith('.js') || file.endsWith('.ts')) && !file.startsWith('_')) {
|
|
220
|
+
const route = path.join(base, file).replace(/\\/g, '/').replace(/\.(js|ts)$/, '');
|
|
221
|
+
const fullPath = `/${route === 'index' ? '' : route}`;
|
|
222
|
+
|
|
223
|
+
// Try to extract schema for playground presets
|
|
224
|
+
let schema = null;
|
|
225
|
+
try {
|
|
226
|
+
const mod = jiti(filePath);
|
|
227
|
+
schema = mod.schema || (mod.default && mod.default.schema);
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
|
|
230
|
+
results.push({ path: fullPath, schema });
|
|
197
231
|
}
|
|
198
232
|
});
|
|
199
233
|
return results;
|
|
@@ -202,51 +236,265 @@ function startServer(port = 3000) {
|
|
|
202
236
|
const routes = getRoutes(apiDir);
|
|
203
237
|
const featureList = Object.entries(config.features)
|
|
204
238
|
.map(([k, v]) => `<li><strong>${k}</strong>: ${v ? '✅' : '❌'}</li>`).join('');
|
|
205
|
-
const routeList = routes.map(r => `<li><a href="${r}">${r}</a></li>`).join('');
|
|
239
|
+
const routeList = routes.map(r => `<li><a href="${r.path}">${r.path}</a></li>`).join('');
|
|
206
240
|
|
|
207
|
-
res.setHeader('Content-Type', 'text/html');
|
|
241
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
208
242
|
return res.end(`
|
|
209
243
|
<!DOCTYPE html>
|
|
210
|
-
<html>
|
|
244
|
+
<html lang="en">
|
|
211
245
|
<head>
|
|
212
|
-
<
|
|
246
|
+
<meta charset="UTF-8">
|
|
247
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
248
|
+
<title>Zerra Dev Dashboard</title>
|
|
213
249
|
<style>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
250
|
+
:root {
|
|
251
|
+
--primary: #0070f3;
|
|
252
|
+
--bg: #fafafa;
|
|
253
|
+
--card-bg: #ffffff;
|
|
254
|
+
--text: #171717;
|
|
255
|
+
--text-light: #666;
|
|
256
|
+
--border: #eaeaea;
|
|
257
|
+
--success: #0070f3;
|
|
258
|
+
--warning: #f5a623;
|
|
259
|
+
--error: #ff0000;
|
|
260
|
+
}
|
|
261
|
+
body {
|
|
262
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
263
|
+
line-height: 1.5;
|
|
264
|
+
background: var(--bg);
|
|
265
|
+
color: var(--text);
|
|
266
|
+
margin: 0;
|
|
267
|
+
padding: 0;
|
|
268
|
+
}
|
|
269
|
+
header {
|
|
270
|
+
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
|
|
271
|
+
color: #fff;
|
|
272
|
+
padding: 16px 40px;
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
justify-content: space-between;
|
|
276
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
277
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
278
|
+
position: sticky;
|
|
279
|
+
top: 0;
|
|
280
|
+
z-index: 100;
|
|
281
|
+
}
|
|
282
|
+
header h1 {
|
|
283
|
+
margin: 0;
|
|
284
|
+
font-size: 1.2rem;
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 12px;
|
|
288
|
+
letter-spacing: 2px;
|
|
289
|
+
font-weight: 800;
|
|
290
|
+
}
|
|
291
|
+
header h1 span.console-text {
|
|
292
|
+
font-weight: 300;
|
|
293
|
+
opacity: 0.6;
|
|
294
|
+
font-size: 0.9rem;
|
|
295
|
+
letter-spacing: 0;
|
|
296
|
+
border-left: 1px solid rgba(255,255,255,0.2);
|
|
297
|
+
padding-left: 12px;
|
|
298
|
+
}
|
|
299
|
+
.badge { font-size: 0.75rem; background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 20px; font-weight: normal; }
|
|
300
|
+
|
|
301
|
+
main { max-width: 1300px; margin: 40px auto; padding: 0 20px; }
|
|
302
|
+
|
|
303
|
+
.grid { display: grid; grid-template-columns: 2.5fr 1fr; gap: 25px; margin-bottom: 20px; }
|
|
304
|
+
@media (max-width: 1024px) { .grid { grid-template-columns: 1fr; } }
|
|
305
|
+
|
|
306
|
+
section {
|
|
307
|
+
background: var(--card-bg);
|
|
308
|
+
padding: 24px;
|
|
309
|
+
border-radius: 12px;
|
|
310
|
+
border: 1px solid var(--border);
|
|
311
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.02);
|
|
312
|
+
}
|
|
313
|
+
h2 { margin-top: 0; font-size: 1.1rem; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; color: var(--text-light); text-transform: uppercase; letter-spacing: 1px; }
|
|
314
|
+
|
|
315
|
+
ul { list-style: none; padding: 0; margin: 0; }
|
|
316
|
+
li { margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
|
|
317
|
+
|
|
318
|
+
.route-link { color: var(--primary); text-decoration: none; font-weight: 500; font-family: monospace; font-size: 1rem; }
|
|
319
|
+
.route-link:hover { text-decoration: underline; }
|
|
320
|
+
|
|
321
|
+
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
|
322
|
+
th { text-align: left; padding: 12px 8px; border-bottom: 2px solid var(--border); font-size: 0.85rem; color: var(--text-light); }
|
|
323
|
+
td { padding: 12px 8px; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
|
324
|
+
|
|
325
|
+
.status-badge {
|
|
326
|
+
padding: 4px 10px;
|
|
327
|
+
border-radius: 6px;
|
|
328
|
+
font-size: 0.75rem;
|
|
329
|
+
font-weight: bold;
|
|
330
|
+
color: #fff;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.env-item { background: #f3f4f6; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 0.85rem; color: #444; }
|
|
334
|
+
|
|
335
|
+
/* Fix for long error messages/stacks */
|
|
336
|
+
pre {
|
|
337
|
+
white-space: pre-wrap !important;
|
|
338
|
+
word-break: break-all !important;
|
|
339
|
+
max-height: 300px !important;
|
|
340
|
+
overflow-y: auto !important;
|
|
341
|
+
margin: 0 !important;
|
|
342
|
+
}
|
|
223
343
|
</style>
|
|
224
344
|
</head>
|
|
225
345
|
<body>
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
346
|
+
<main style="margin-top: 20px;">
|
|
347
|
+
<div class="grid">
|
|
348
|
+
<section>
|
|
349
|
+
<h2>📂 Active Routes & Playground</h2>
|
|
350
|
+
<div style="display: flex; flex-direction: column; gap: 15px;">
|
|
351
|
+
${routes.length > 0 ? routes.map(r => {
|
|
352
|
+
const sampleBody = r.schema ? JSON.stringify(Object.fromEntries(
|
|
353
|
+
Object.entries(r.schema).map(([k, t]) => [k, t === 'number' ? 0 : t === 'boolean' ? false : 'text'])
|
|
354
|
+
)) : '{}';
|
|
355
|
+
|
|
356
|
+
return `
|
|
357
|
+
<div style="border: 1px solid var(--border); border-radius: 8px; padding: 15px;">
|
|
358
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
|
359
|
+
<a href="${r.path}" class="route-link" style="font-size: 1.1rem;">${r.path}</a>
|
|
360
|
+
${r.schema ? '<span class="badge" style="background:#eee; color:#666;">Has Schema</span>' : ''}
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px;">
|
|
364
|
+
<select id="method-${r.path}" style="padding: 4px 8px; border-radius: 4px; border: 1px solid var(--border); background: #fff; font-family: inherit;">
|
|
365
|
+
<option value="GET">GET</option>
|
|
366
|
+
<option value="POST">POST</option>
|
|
367
|
+
<option value="PUT">PUT</option>
|
|
368
|
+
<option value="PATCH">PATCH</option>
|
|
369
|
+
<option value="DELETE">DELETE</option>
|
|
370
|
+
</select>
|
|
371
|
+
<input type="text" id="body-${r.path}" value='${sampleBody}' placeholder='{"key": "value"}' style="flex-grow: 1; padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border); font-family: monospace; font-size: 0.8rem;">
|
|
372
|
+
<button onclick="testRoute('${r.path}')" style="background: var(--primary); color: #fff; border: none; padding: 5px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.8rem;">SEND</button>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div id="res-${r.path}" style="display: none; background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 6px; font-family: monospace; font-size: 0.85rem; overflow-x: auto; margin-top: 10px; position: relative;">
|
|
376
|
+
<div id="status-${r.path}" style="position: absolute; top: 8px; right: 8px; font-size: 0.7rem; font-weight: bold;"></div>
|
|
377
|
+
<pre style="margin: 0;"></pre>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
`}).join('') : '<div style="color:#999">No routes found in /api</div>'}
|
|
381
|
+
</div>
|
|
382
|
+
</section>
|
|
383
|
+
|
|
384
|
+
<section>
|
|
385
|
+
<h2>⚙️ Features</h2>
|
|
386
|
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
|
387
|
+
${Object.entries(config.features).map(([k, v]) => `
|
|
388
|
+
<div style="display: flex; align-items: center; justify-content: space-between; font-size: 0.9rem; border-bottom: 1px solid #f9f9f9; padding-bottom: 8px;">
|
|
389
|
+
<span style="color: ${v ? 'inherit' : '#999'}; font-weight: 500;">${k}</span>
|
|
390
|
+
<span>${v ? '✅' : '❌'}</span>
|
|
391
|
+
</div>
|
|
392
|
+
`).join('')}
|
|
393
|
+
</div>
|
|
394
|
+
</section>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<section style="margin-bottom: 20px;">
|
|
398
|
+
<h2>📊 Recent Activity</h2>
|
|
399
|
+
<table>
|
|
400
|
+
<thead>
|
|
401
|
+
<tr>
|
|
402
|
+
<th>METHOD</th>
|
|
403
|
+
<th>PATH</th>
|
|
404
|
+
<th>STATUS</th>
|
|
405
|
+
<th>TIME</th>
|
|
406
|
+
<th>DURATION</th>
|
|
407
|
+
</tr>
|
|
408
|
+
</thead>
|
|
409
|
+
<tbody>
|
|
410
|
+
${recentRequests.map(req => {
|
|
411
|
+
const color = req.statusCode >= 500 ? 'var(--error)' : req.statusCode >= 400 ? 'var(--warning)' : 'var(--success)';
|
|
412
|
+
return `
|
|
413
|
+
<tr>
|
|
414
|
+
<td><strong>${req.method}</strong></td>
|
|
415
|
+
<td style="font-family: monospace; color: #444;">${req.path}</td>
|
|
416
|
+
<td><span class="status-badge" style="background: ${color}">${req.statusCode}</span></td>
|
|
417
|
+
<td style="color: #888;">${req.timestamp}</td>
|
|
418
|
+
<td style="color: #888;">${req.duration}ms</td>
|
|
419
|
+
</tr>
|
|
420
|
+
`;
|
|
421
|
+
}).join('') || '<tr><td colspan="5" style="text-align: center; padding: 40px; color: #999;">Waiting for requests...</td></tr>'}
|
|
422
|
+
</tbody>
|
|
423
|
+
</table>
|
|
424
|
+
</section>
|
|
425
|
+
|
|
426
|
+
<section>
|
|
427
|
+
<h2>🔐 Environment</h2>
|
|
428
|
+
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
|
429
|
+
${Object.keys(process.env).filter(k => {
|
|
430
|
+
const isCustom = customEnvKeys.has(k);
|
|
431
|
+
const isImportant = ['PORT', 'NODE_ENV'].includes(k);
|
|
432
|
+
const isSystem = /^(ALLUSERSPROFILE|APPDATA|COMPUTERNAME|ComSpec|Common|DriverData|HOMEDRIVE|HOMEPATH|LOCALAPPDATA|LOGONSERVER|NUMBER_OF_PROCESSORS|OS|Path|PATHEXT|PROCESSOR|Program|PSModulePath|PUBLIC|System|TEMP|TMP|USER|windir|ZES_|VSCODE_|ANTIGRAVITY_)/i.test(k);
|
|
433
|
+
return (isCustom || isImportant) && !isSystem;
|
|
434
|
+
}).map(k => `
|
|
435
|
+
<div style="background: #f9f9f9; border: 1px solid #eee; padding: 10px 15px; border-radius: 8px;">
|
|
436
|
+
<div style="font-size: 0.7rem; color: #999; margin-bottom: 4px; font-weight: bold;">${k}</div>
|
|
437
|
+
<div class="env-item">${process.env[k]}</div>
|
|
438
|
+
</div>
|
|
439
|
+
`).join('') || '<div style="color:#999">No custom variables loaded</div>'}
|
|
440
|
+
</div>
|
|
441
|
+
</section>
|
|
442
|
+
</main>
|
|
443
|
+
|
|
444
|
+
<script>
|
|
445
|
+
async function testRoute(path) {
|
|
446
|
+
const method = document.getElementById('method-' + path).value;
|
|
447
|
+
const bodyStr = document.getElementById('body-' + path).value;
|
|
448
|
+
const resDiv = document.getElementById('res-' + path);
|
|
449
|
+
const statusDiv = document.getElementById('status-' + path);
|
|
450
|
+
const pre = resDiv.querySelector('pre');
|
|
451
|
+
|
|
452
|
+
resDiv.style.display = 'block';
|
|
453
|
+
pre.innerText = 'Sending request...';
|
|
454
|
+
statusDiv.innerText = '';
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const options = { method, headers: {} };
|
|
458
|
+
if (['POST', 'PUT', 'PATCH'].includes(method) && bodyStr) {
|
|
459
|
+
options.headers['Content-Type'] = 'application/json';
|
|
460
|
+
options.body = bodyStr;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const start = Date.now();
|
|
464
|
+
const response = await fetch(path, options);
|
|
465
|
+
const duration = Date.now() - start;
|
|
466
|
+
const data = await response.json().catch(() => null);
|
|
467
|
+
|
|
468
|
+
statusDiv.innerText = response.status + ' (' + duration + 'ms)';
|
|
469
|
+
statusDiv.style.color = response.status >= 400 ? '#ff4d4f' : '#52c41a';
|
|
470
|
+
pre.innerText = JSON.stringify(data, null, 2) || 'No response body';
|
|
471
|
+
} catch (err) {
|
|
472
|
+
statusDiv.innerText = 'ERROR';
|
|
473
|
+
statusDiv.style.color = '#ff4d4f';
|
|
474
|
+
pre.innerText = err.message;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Soft refresh every 5 seconds
|
|
479
|
+
let refreshTimeout = setTimeout(() => {
|
|
480
|
+
window.location.reload();
|
|
481
|
+
}, 5000);
|
|
482
|
+
|
|
483
|
+
// Pause refresh if user is interacting with playground
|
|
484
|
+
document.addEventListener('mousedown', () => {
|
|
485
|
+
clearTimeout(refreshTimeout);
|
|
486
|
+
refreshTimeout = setTimeout(() => window.location.reload(), 15000);
|
|
487
|
+
});
|
|
488
|
+
</script>
|
|
244
489
|
</body>
|
|
245
490
|
</html>
|
|
246
491
|
`);
|
|
247
492
|
}
|
|
248
493
|
|
|
249
494
|
let filePath = path.join(apiDir, `${cleanPath}.js`);
|
|
495
|
+
if (!fs.existsSync(filePath)) {
|
|
496
|
+
filePath = path.join(apiDir, `${cleanPath}.ts`);
|
|
497
|
+
}
|
|
250
498
|
|
|
251
499
|
// 6. Enhanced DX: Dynamic Routing ([id].js)
|
|
252
500
|
if (config.features.dynamicRouting && !fs.existsSync(filePath)) {
|
|
@@ -266,9 +514,9 @@ function startServer(port = 3000) {
|
|
|
266
514
|
|
|
267
515
|
// Look for dynamic parameter [param]
|
|
268
516
|
if (!match) {
|
|
269
|
-
match = files.find(f => isLast ? (f.startsWith('[') && f.endsWith('].js')) : (f.startsWith('[') && f.endsWith(']') && fs.statSync(path.join(currentDir, f)).isDirectory()));
|
|
517
|
+
match = files.find(f => isLast ? (f.startsWith('[') && (f.endsWith('].js') || f.endsWith('].ts'))) : (f.startsWith('[') && f.endsWith(']') && fs.statSync(path.join(currentDir, f)).isDirectory()));
|
|
270
518
|
if (match) {
|
|
271
|
-
const paramName = isLast ? match.slice(1,
|
|
519
|
+
const paramName = isLast ? match.slice(1, match.lastIndexOf('].')) : match.slice(1, -1);
|
|
272
520
|
req.params[paramName] = part;
|
|
273
521
|
}
|
|
274
522
|
}
|
|
@@ -301,7 +549,9 @@ function startServer(port = 3000) {
|
|
|
301
549
|
|
|
302
550
|
if (config.features.middleware) {
|
|
303
551
|
while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
|
|
304
|
-
|
|
552
|
+
let mwPath = path.join(currentPath, '_middleware.js');
|
|
553
|
+
if (!fs.existsSync(mwPath)) mwPath = path.join(currentPath, '_middleware.ts');
|
|
554
|
+
|
|
305
555
|
if (fs.existsSync(mwPath)) {
|
|
306
556
|
middlewarePaths.unshift(mwPath); // Run top-down
|
|
307
557
|
}
|
|
@@ -326,22 +576,30 @@ function startServer(port = 3000) {
|
|
|
326
576
|
if (middlewareIndex < middlewarePaths.length) {
|
|
327
577
|
const mwPath = middlewarePaths[middlewareIndex++];
|
|
328
578
|
delete require.cache[require.resolve(mwPath)];
|
|
329
|
-
const mw =
|
|
330
|
-
if (typeof mw === 'function') {
|
|
331
|
-
|
|
579
|
+
const mw = jiti(mwPath);
|
|
580
|
+
if (typeof mw === 'function' || (mw && typeof mw.default === 'function')) {
|
|
581
|
+
const actualMw = mw.default || mw;
|
|
582
|
+
await actualMw(req, res, runNext);
|
|
332
583
|
} else {
|
|
333
584
|
await runNext();
|
|
334
585
|
}
|
|
335
586
|
} else {
|
|
336
587
|
delete require.cache[require.resolve(filePath)];
|
|
337
|
-
const handler =
|
|
588
|
+
const handler = jiti(filePath);
|
|
338
589
|
|
|
339
590
|
if (typeof handler === "function" || (handler && typeof handler.default === "function")) {
|
|
340
591
|
const actualHandler = handler.default || handler;
|
|
341
592
|
|
|
342
593
|
// 9. Enhanced DX: Input Validation
|
|
343
594
|
const schema = handler.schema;
|
|
344
|
-
if (config.features.validation && schema
|
|
595
|
+
if (config.features.validation && schema) {
|
|
596
|
+
if (!req.body || typeof req.body !== 'object') {
|
|
597
|
+
return res.status(400).json({
|
|
598
|
+
error: "Validation Failed",
|
|
599
|
+
details: ["Request body is required for this route."]
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
345
603
|
const errors = [];
|
|
346
604
|
for (const [key, type] of Object.entries(schema)) {
|
|
347
605
|
if (typeof req.body[key] !== type) {
|
|
@@ -379,9 +637,10 @@ function startServer(port = 3000) {
|
|
|
379
637
|
if (fs.existsSync(errorHandlerPath)) {
|
|
380
638
|
try {
|
|
381
639
|
delete require.cache[require.resolve(errorHandlerPath)];
|
|
382
|
-
const errorHandler =
|
|
383
|
-
|
|
384
|
-
|
|
640
|
+
const errorHandler = jiti(errorHandlerPath);
|
|
641
|
+
const actualErrorHandler = errorHandler.default || errorHandler;
|
|
642
|
+
if (typeof actualErrorHandler === 'function') {
|
|
643
|
+
return await actualErrorHandler(err, req, res);
|
|
385
644
|
}
|
|
386
645
|
} catch (e) {
|
|
387
646
|
console.error("❌ Error in custom error handler:", e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zerra-core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"type": "commonjs",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"busboy": "^1.6.0"
|
|
14
|
+
"busboy": "^1.6.0",
|
|
15
|
+
"jiti": "^2.6.1"
|
|
15
16
|
}
|
|
16
17
|
}
|