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.
Files changed (3) hide show
  1. package/index.d.ts +47 -0
  2. package/index.js +408 -14
  3. 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
- if (!process.env.hasOwnProperty(key)) process.env[key] = value;
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} ${req.path || url} ➜ ${statusColor}${res.statusCode}${resetColor} (${duration}ms)`);
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, -4) : match.slice(1, -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
- const mwPath = path.join(currentPath, '_middleware.js');
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 = require(mwPath);
231
- if (typeof mw === 'function') {
232
- await mw(req, res, runNext);
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 = require(filePath);
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 && typeof req.body === 'object' && req.body !== null) {
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
- await runNext();
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.0",
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
  }