zerra-core 1.1.0 → 1.2.0
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.js +138 -3
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -11,8 +11,11 @@ function startServer(port = 3000) {
|
|
|
11
11
|
middleware: true,
|
|
12
12
|
dotenv: true,
|
|
13
13
|
validation: true,
|
|
14
|
-
multipart: true
|
|
15
|
-
|
|
14
|
+
multipart: true,
|
|
15
|
+
errors: true,
|
|
16
|
+
dashboard: true
|
|
17
|
+
},
|
|
18
|
+
plugins: []
|
|
16
19
|
};
|
|
17
20
|
if (fs.existsSync(configPath)) {
|
|
18
21
|
try {
|
|
@@ -42,6 +45,31 @@ function startServer(port = 3000) {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
const globalMiddleware = [];
|
|
49
|
+
const resDecorators = {};
|
|
50
|
+
const reqDecorators = {};
|
|
51
|
+
|
|
52
|
+
const zerra = {
|
|
53
|
+
use: (fn) => globalMiddleware.push(fn),
|
|
54
|
+
decorate: (target, name, fn) => {
|
|
55
|
+
if (target === 'res') resDecorators[name] = fn;
|
|
56
|
+
if (target === 'req') reqDecorators[name] = fn;
|
|
57
|
+
},
|
|
58
|
+
config
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Load Plugins
|
|
62
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
63
|
+
config.plugins.forEach(pluginPath => {
|
|
64
|
+
try {
|
|
65
|
+
const plugin = require(path.isAbsolute(pluginPath) ? pluginPath : path.join(process.cwd(), pluginPath));
|
|
66
|
+
if (typeof plugin === 'function') plugin(zerra);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error(`❌ Failed to load plugin: ${pluginPath}`, e);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
const apiDir = path.join(process.cwd(), "api");
|
|
46
74
|
|
|
47
75
|
const server = http.createServer(async (req, res) => {
|
|
@@ -76,6 +104,10 @@ function startServer(port = 3000) {
|
|
|
76
104
|
req.query = Object.fromEntries(parsedUrl.searchParams);
|
|
77
105
|
req.path = parsedUrl.pathname;
|
|
78
106
|
|
|
107
|
+
// Apply Decorators
|
|
108
|
+
Object.entries(resDecorators).forEach(([name, fn]) => { res[name] = fn.bind(res); });
|
|
109
|
+
Object.entries(reqDecorators).forEach(([name, fn]) => { req[name] = fn.bind(req); });
|
|
110
|
+
|
|
79
111
|
// 3. Enhanced DX: CORS Helper
|
|
80
112
|
res.cors = (options = { origin: '*', methods: 'GET,POST,PUT,DELETE,OPTIONS' }) => {
|
|
81
113
|
res.setHeader('Access-Control-Allow-Origin', options.origin);
|
|
@@ -147,6 +179,73 @@ function startServer(port = 3000) {
|
|
|
147
179
|
|
|
148
180
|
req.params = {};
|
|
149
181
|
const cleanPath = req.path === "/" ? "/index" : req.path;
|
|
182
|
+
|
|
183
|
+
// 10. Enhanced DX: Dev Dashboard
|
|
184
|
+
if (config.features.dashboard && cleanPath === '/__zerra') {
|
|
185
|
+
const getRoutes = (dir, base = '') => {
|
|
186
|
+
let results = [];
|
|
187
|
+
if (!fs.existsSync(dir)) return results;
|
|
188
|
+
const list = fs.readdirSync(dir);
|
|
189
|
+
list.forEach(file => {
|
|
190
|
+
const filePath = path.join(dir, file);
|
|
191
|
+
const stat = fs.statSync(filePath);
|
|
192
|
+
if (stat && stat.isDirectory()) {
|
|
193
|
+
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('.js', '');
|
|
196
|
+
results.push(route === 'index' ? '/' : `/${route}`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return results;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const routes = getRoutes(apiDir);
|
|
203
|
+
const featureList = Object.entries(config.features)
|
|
204
|
+
.map(([k, v]) => `<li><strong>${k}</strong>: ${v ? '✅' : '❌'}</li>`).join('');
|
|
205
|
+
const routeList = routes.map(r => `<li><a href="${r}">${r}</a></li>`).join('');
|
|
206
|
+
|
|
207
|
+
res.setHeader('Content-Type', 'text/html');
|
|
208
|
+
return res.end(`
|
|
209
|
+
<!DOCTYPE html>
|
|
210
|
+
<html>
|
|
211
|
+
<head>
|
|
212
|
+
<title>Zerra Dashboard</title>
|
|
213
|
+
<style>
|
|
214
|
+
body { font-family: sans-serif; line-height: 1.6; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #333; background: #f9f9f9; }
|
|
215
|
+
h1 { color: #000; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
|
216
|
+
section { background: #fff; padding: 20px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 20px; }
|
|
217
|
+
h2 { margin-top: 0; font-size: 1.2rem; }
|
|
218
|
+
ul { padding-left: 20px; }
|
|
219
|
+
li { margin-bottom: 5px; }
|
|
220
|
+
a { color: #0070f3; text-decoration: none; }
|
|
221
|
+
a:hover { text-decoration: underline; }
|
|
222
|
+
.badge { font-size: 0.8rem; background: #000; color: #fff; padding: 2px 6px; border-radius: 3px; vertical-align: middle; }
|
|
223
|
+
</style>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<h1>🚀 Zerra Dev Dashboard <span class="badge">v1.1.1</span></h1>
|
|
227
|
+
|
|
228
|
+
<section>
|
|
229
|
+
<h2>📂 Active Routes</h2>
|
|
230
|
+
<ul>${routeList || '<li>No routes found in /api</li>'}</ul>
|
|
231
|
+
</section>
|
|
232
|
+
|
|
233
|
+
<section>
|
|
234
|
+
<h2>⚙️ Enabled Features</h2>
|
|
235
|
+
<ul>${featureList}</ul>
|
|
236
|
+
</section>
|
|
237
|
+
|
|
238
|
+
<section>
|
|
239
|
+
<h2>🔐 Environment Variables</h2>
|
|
240
|
+
<ul>${Object.keys(process.env).filter(k => !k.startsWith('npm_') && !k.startsWith('NODE_')).map(k => `<li>${k}</li>`).join('') || '<li>No custom env vars loaded</li>'}</ul>
|
|
241
|
+
</section>
|
|
242
|
+
|
|
243
|
+
<p><small>Zerra Engine is running in development mode.</small></p>
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
|
|
150
249
|
let filePath = path.join(apiDir, `${cleanPath}.js`);
|
|
151
250
|
|
|
152
251
|
// 6. Enhanced DX: Dynamic Routing ([id].js)
|
|
@@ -261,9 +360,45 @@ function startServer(port = 3000) {
|
|
|
261
360
|
}
|
|
262
361
|
};
|
|
263
362
|
|
|
264
|
-
|
|
363
|
+
// 11. Enhanced DX: Global Middleware (Plugins)
|
|
364
|
+
let globalIndex = 0;
|
|
365
|
+
const runGlobal = async () => {
|
|
366
|
+
if (globalIndex < globalMiddleware.length) {
|
|
367
|
+
await globalMiddleware[globalIndex++](req, res, runGlobal);
|
|
368
|
+
} else {
|
|
369
|
+
await runNext();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
await runGlobal();
|
|
265
374
|
|
|
266
375
|
} catch (err) {
|
|
376
|
+
if (config.features.errors) {
|
|
377
|
+
// Check for custom _error.js handler
|
|
378
|
+
const errorHandlerPath = path.join(apiDir, '_error.js');
|
|
379
|
+
if (fs.existsSync(errorHandlerPath)) {
|
|
380
|
+
try {
|
|
381
|
+
delete require.cache[require.resolve(errorHandlerPath)];
|
|
382
|
+
const errorHandler = require(errorHandlerPath);
|
|
383
|
+
if (typeof errorHandler === 'function') {
|
|
384
|
+
return await errorHandler(err, req, res);
|
|
385
|
+
}
|
|
386
|
+
} catch (e) {
|
|
387
|
+
console.error("❌ Error in custom error handler:", e);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const statusCode = err.status || 500;
|
|
392
|
+
const message = err.message || "Internal Server Error";
|
|
393
|
+
|
|
394
|
+
return res.status(statusCode).json({
|
|
395
|
+
error: statusCode >= 500 ? "Runtime Error" : "Request Error",
|
|
396
|
+
message: message,
|
|
397
|
+
stack: process.env.NODE_ENV === 'development' || !process.env.NODE_ENV ? err.stack : undefined
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Fallback if errors feature is disabled
|
|
267
402
|
res.status(500).json({ error: "Runtime Error", message: err.message });
|
|
268
403
|
}
|
|
269
404
|
} else {
|