zerra-core 1.1.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 +408 -14
- 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: {
|
|
@@ -11,8 +12,11 @@ function startServer(port = 3000) {
|
|
|
11
12
|
middleware: true,
|
|
12
13
|
dotenv: true,
|
|
13
14
|
validation: true,
|
|
14
|
-
multipart: true
|
|
15
|
-
|
|
15
|
+
multipart: true,
|
|
16
|
+
errors: true,
|
|
17
|
+
dashboard: true
|
|
18
|
+
},
|
|
19
|
+
plugins: []
|
|
16
20
|
};
|
|
17
21
|
if (fs.existsSync(configPath)) {
|
|
18
22
|
try {
|
|
@@ -23,6 +27,10 @@ function startServer(port = 3000) {
|
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
const customEnvKeys = new Set();
|
|
31
|
+
const recentRequests = [];
|
|
32
|
+
const MAX_LOGS = 20;
|
|
33
|
+
|
|
26
34
|
// 8. Enhanced DX: Auto-load .env files
|
|
27
35
|
if (config.features.dotenv) {
|
|
28
36
|
const envPath = path.join(process.cwd(), '.env');
|
|
@@ -36,12 +44,41 @@ function startServer(port = 3000) {
|
|
|
36
44
|
// Remove quotes
|
|
37
45
|
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
38
46
|
else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
39
|
-
|
|
47
|
+
|
|
48
|
+
if (!process.env.hasOwnProperty(key)) {
|
|
49
|
+
process.env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
customEnvKeys.add(key);
|
|
40
52
|
}
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
const globalMiddleware = [];
|
|
58
|
+
const resDecorators = {};
|
|
59
|
+
const reqDecorators = {};
|
|
60
|
+
|
|
61
|
+
const zerra = {
|
|
62
|
+
use: (fn) => globalMiddleware.push(fn),
|
|
63
|
+
decorate: (target, name, fn) => {
|
|
64
|
+
if (target === 'res') resDecorators[name] = fn;
|
|
65
|
+
if (target === 'req') reqDecorators[name] = fn;
|
|
66
|
+
},
|
|
67
|
+
config
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Load Plugins
|
|
71
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
72
|
+
config.plugins.forEach(pluginPath => {
|
|
73
|
+
try {
|
|
74
|
+
const plugin = jiti(path.isAbsolute(pluginPath) ? pluginPath : path.join(process.cwd(), pluginPath));
|
|
75
|
+
if (typeof plugin === 'function') plugin(zerra);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error(`❌ Failed to load plugin: ${pluginPath}`, e);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
45
82
|
const apiDir = path.join(process.cwd(), "api");
|
|
46
83
|
|
|
47
84
|
const server = http.createServer(async (req, res) => {
|
|
@@ -51,12 +88,28 @@ function startServer(port = 3000) {
|
|
|
51
88
|
// 1. Enhanced DX: Beautiful Request Logging
|
|
52
89
|
const originalEnd = res.end;
|
|
53
90
|
res.end = function (...args) {
|
|
91
|
+
const duration = Date.now() - startTime;
|
|
92
|
+
const path = req.path || url;
|
|
93
|
+
|
|
94
|
+
// Log to terminal
|
|
54
95
|
if (config.features.logging) {
|
|
55
|
-
const duration = Date.now() - startTime;
|
|
56
96
|
const statusColor = res.statusCode >= 500 ? '\x1b[31m' : res.statusCode >= 400 ? '\x1b[33m' : '\x1b[32m';
|
|
57
97
|
const resetColor = '\x1b[0m';
|
|
58
|
-
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();
|
|
59
111
|
}
|
|
112
|
+
|
|
60
113
|
return originalEnd.apply(this, args);
|
|
61
114
|
};
|
|
62
115
|
|
|
@@ -76,6 +129,10 @@ function startServer(port = 3000) {
|
|
|
76
129
|
req.query = Object.fromEntries(parsedUrl.searchParams);
|
|
77
130
|
req.path = parsedUrl.pathname;
|
|
78
131
|
|
|
132
|
+
// Apply Decorators
|
|
133
|
+
Object.entries(resDecorators).forEach(([name, fn]) => { res[name] = fn.bind(res); });
|
|
134
|
+
Object.entries(reqDecorators).forEach(([name, fn]) => { req[name] = fn.bind(req); });
|
|
135
|
+
|
|
79
136
|
// 3. Enhanced DX: CORS Helper
|
|
80
137
|
res.cors = (options = { origin: '*', methods: 'GET,POST,PUT,DELETE,OPTIONS' }) => {
|
|
81
138
|
res.setHeader('Access-Control-Allow-Origin', options.origin);
|
|
@@ -147,7 +204,297 @@ function startServer(port = 3000) {
|
|
|
147
204
|
|
|
148
205
|
req.params = {};
|
|
149
206
|
const cleanPath = req.path === "/" ? "/index" : req.path;
|
|
207
|
+
|
|
208
|
+
// 10. Enhanced DX: Dev Dashboard
|
|
209
|
+
if (config.features.dashboard && cleanPath === '/__zerra') {
|
|
210
|
+
const getRoutes = (dir, base = '') => {
|
|
211
|
+
let results = [];
|
|
212
|
+
if (!fs.existsSync(dir)) return results;
|
|
213
|
+
const list = fs.readdirSync(dir);
|
|
214
|
+
list.forEach(file => {
|
|
215
|
+
const filePath = path.join(dir, file);
|
|
216
|
+
const stat = fs.statSync(filePath);
|
|
217
|
+
if (stat && stat.isDirectory()) {
|
|
218
|
+
results = results.concat(getRoutes(filePath, path.join(base, file)));
|
|
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 });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
return results;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const routes = getRoutes(apiDir);
|
|
237
|
+
const featureList = Object.entries(config.features)
|
|
238
|
+
.map(([k, v]) => `<li><strong>${k}</strong>: ${v ? '✅' : '❌'}</li>`).join('');
|
|
239
|
+
const routeList = routes.map(r => `<li><a href="${r.path}">${r.path}</a></li>`).join('');
|
|
240
|
+
|
|
241
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
242
|
+
return res.end(`
|
|
243
|
+
<!DOCTYPE html>
|
|
244
|
+
<html lang="en">
|
|
245
|
+
<head>
|
|
246
|
+
<meta charset="UTF-8">
|
|
247
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
248
|
+
<title>Zerra Dev Dashboard</title>
|
|
249
|
+
<style>
|
|
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
|
+
}
|
|
343
|
+
</style>
|
|
344
|
+
</head>
|
|
345
|
+
<body>
|
|
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>
|
|
489
|
+
</body>
|
|
490
|
+
</html>
|
|
491
|
+
`);
|
|
492
|
+
}
|
|
493
|
+
|
|
150
494
|
let filePath = path.join(apiDir, `${cleanPath}.js`);
|
|
495
|
+
if (!fs.existsSync(filePath)) {
|
|
496
|
+
filePath = path.join(apiDir, `${cleanPath}.ts`);
|
|
497
|
+
}
|
|
151
498
|
|
|
152
499
|
// 6. Enhanced DX: Dynamic Routing ([id].js)
|
|
153
500
|
if (config.features.dynamicRouting && !fs.existsSync(filePath)) {
|
|
@@ -167,9 +514,9 @@ function startServer(port = 3000) {
|
|
|
167
514
|
|
|
168
515
|
// Look for dynamic parameter [param]
|
|
169
516
|
if (!match) {
|
|
170
|
-
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()));
|
|
171
518
|
if (match) {
|
|
172
|
-
const paramName = isLast ? match.slice(1,
|
|
519
|
+
const paramName = isLast ? match.slice(1, match.lastIndexOf('].')) : match.slice(1, -1);
|
|
173
520
|
req.params[paramName] = part;
|
|
174
521
|
}
|
|
175
522
|
}
|
|
@@ -202,7 +549,9 @@ function startServer(port = 3000) {
|
|
|
202
549
|
|
|
203
550
|
if (config.features.middleware) {
|
|
204
551
|
while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
|
|
205
|
-
|
|
552
|
+
let mwPath = path.join(currentPath, '_middleware.js');
|
|
553
|
+
if (!fs.existsSync(mwPath)) mwPath = path.join(currentPath, '_middleware.ts');
|
|
554
|
+
|
|
206
555
|
if (fs.existsSync(mwPath)) {
|
|
207
556
|
middlewarePaths.unshift(mwPath); // Run top-down
|
|
208
557
|
}
|
|
@@ -227,22 +576,30 @@ function startServer(port = 3000) {
|
|
|
227
576
|
if (middlewareIndex < middlewarePaths.length) {
|
|
228
577
|
const mwPath = middlewarePaths[middlewareIndex++];
|
|
229
578
|
delete require.cache[require.resolve(mwPath)];
|
|
230
|
-
const mw =
|
|
231
|
-
if (typeof mw === 'function') {
|
|
232
|
-
|
|
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);
|
|
233
583
|
} else {
|
|
234
584
|
await runNext();
|
|
235
585
|
}
|
|
236
586
|
} else {
|
|
237
587
|
delete require.cache[require.resolve(filePath)];
|
|
238
|
-
const handler =
|
|
588
|
+
const handler = jiti(filePath);
|
|
239
589
|
|
|
240
590
|
if (typeof handler === "function" || (handler && typeof handler.default === "function")) {
|
|
241
591
|
const actualHandler = handler.default || handler;
|
|
242
592
|
|
|
243
593
|
// 9. Enhanced DX: Input Validation
|
|
244
594
|
const schema = handler.schema;
|
|
245
|
-
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
|
+
|
|
246
603
|
const errors = [];
|
|
247
604
|
for (const [key, type] of Object.entries(schema)) {
|
|
248
605
|
if (typeof req.body[key] !== type) {
|
|
@@ -261,9 +618,46 @@ function startServer(port = 3000) {
|
|
|
261
618
|
}
|
|
262
619
|
};
|
|
263
620
|
|
|
264
|
-
|
|
621
|
+
// 11. Enhanced DX: Global Middleware (Plugins)
|
|
622
|
+
let globalIndex = 0;
|
|
623
|
+
const runGlobal = async () => {
|
|
624
|
+
if (globalIndex < globalMiddleware.length) {
|
|
625
|
+
await globalMiddleware[globalIndex++](req, res, runGlobal);
|
|
626
|
+
} else {
|
|
627
|
+
await runNext();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
await runGlobal();
|
|
265
632
|
|
|
266
633
|
} catch (err) {
|
|
634
|
+
if (config.features.errors) {
|
|
635
|
+
// Check for custom _error.js handler
|
|
636
|
+
const errorHandlerPath = path.join(apiDir, '_error.js');
|
|
637
|
+
if (fs.existsSync(errorHandlerPath)) {
|
|
638
|
+
try {
|
|
639
|
+
delete require.cache[require.resolve(errorHandlerPath)];
|
|
640
|
+
const errorHandler = jiti(errorHandlerPath);
|
|
641
|
+
const actualErrorHandler = errorHandler.default || errorHandler;
|
|
642
|
+
if (typeof actualErrorHandler === 'function') {
|
|
643
|
+
return await actualErrorHandler(err, req, res);
|
|
644
|
+
}
|
|
645
|
+
} catch (e) {
|
|
646
|
+
console.error("❌ Error in custom error handler:", e);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const statusCode = err.status || 500;
|
|
651
|
+
const message = err.message || "Internal Server Error";
|
|
652
|
+
|
|
653
|
+
return res.status(statusCode).json({
|
|
654
|
+
error: statusCode >= 500 ? "Runtime Error" : "Request Error",
|
|
655
|
+
message: message,
|
|
656
|
+
stack: process.env.NODE_ENV === 'development' || !process.env.NODE_ENV ? err.stack : undefined
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Fallback if errors feature is disabled
|
|
267
661
|
res.status(500).json({ error: "Runtime Error", message: err.message });
|
|
268
662
|
}
|
|
269
663
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zerra-core",
|
|
3
|
-
"version": "1.1
|
|
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
|
}
|