zerra-core 1.0.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.
Files changed (2) hide show
  1. package/index.js +390 -18
  2. package/package.json +5 -2
package/index.js CHANGED
@@ -3,34 +3,406 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
 
5
5
  function startServer(port = 3000) {
6
+ const configPath = path.join(process.cwd(), 'zerra.config.json');
7
+ let config = {
8
+ features: {
9
+ logging: true,
10
+ dynamicRouting: true,
11
+ middleware: true,
12
+ dotenv: true,
13
+ validation: true,
14
+ multipart: true,
15
+ errors: true,
16
+ dashboard: true
17
+ },
18
+ plugins: []
19
+ };
20
+ if (fs.existsSync(configPath)) {
21
+ try {
22
+ const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
23
+ config.features = { ...config.features, ...(userConfig.features || {}) };
24
+ } catch (e) {
25
+ console.warn("⚠️ Invalid zerra.config.json. Using defaults.");
26
+ }
27
+ }
28
+
29
+ // 8. Enhanced DX: Auto-load .env files
30
+ if (config.features.dotenv) {
31
+ const envPath = path.join(process.cwd(), '.env');
32
+ if (fs.existsSync(envPath)) {
33
+ const envFile = fs.readFileSync(envPath, 'utf8');
34
+ envFile.split('\n').forEach(line => {
35
+ const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
36
+ if (match) {
37
+ const key = match[1];
38
+ let value = match[2] || '';
39
+ // Remove quotes
40
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
41
+ else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
42
+ if (!process.env.hasOwnProperty(key)) process.env[key] = value;
43
+ }
44
+ });
45
+ }
46
+ }
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
+
6
73
  const apiDir = path.join(process.cwd(), "api");
7
74
 
8
- const server = http.createServer((req, res) => {
75
+ const server = http.createServer(async (req, res) => {
9
76
  const { url, method } = req;
10
-
11
- // Simple path cleaning: remove trailing slashes and get file path
12
- const cleanPath = url === "/" ? "/index" : url;
13
- const filePath = path.join(apiDir, `${cleanPath}.js`);
77
+ const startTime = Date.now();
14
78
 
15
- if (fs.existsSync(filePath)) {
16
- try {
17
- // Clear cache for hot-reloading in dev (optional, but good for DX)
18
- delete require.cache[require.resolve(filePath)];
19
- const handler = require(filePath);
79
+ // 1. Enhanced DX: Beautiful Request Logging
80
+ const originalEnd = res.end;
81
+ res.end = function (...args) {
82
+ if (config.features.logging) {
83
+ const duration = Date.now() - startTime;
84
+ const statusColor = res.statusCode >= 500 ? '\x1b[31m' : res.statusCode >= 400 ? '\x1b[33m' : '\x1b[32m';
85
+ const resetColor = '\x1b[0m';
86
+ console.log(`${statusColor}[${method}]${resetColor} ${req.path || url} ➜ ${statusColor}${res.statusCode}${resetColor} (${duration}ms)`);
87
+ }
88
+ return originalEnd.apply(this, args);
89
+ };
90
+
91
+ // 2. Enhanced DX: Add res.status and res.json helpers
92
+ res.status = (code) => {
93
+ res.statusCode = code;
94
+ return res;
95
+ };
96
+
97
+ res.json = (data) => {
98
+ res.setHeader("Content-Type", "application/json");
99
+ res.end(JSON.stringify(data));
100
+ };
101
+
102
+ // 2. Enhanced DX: Parse query parameters
103
+ const parsedUrl = new URL(url, `http://localhost:${port}`);
104
+ req.query = Object.fromEntries(parsedUrl.searchParams);
105
+ req.path = parsedUrl.pathname;
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
+
111
+ // 3. Enhanced DX: CORS Helper
112
+ res.cors = (options = { origin: '*', methods: 'GET,POST,PUT,DELETE,OPTIONS' }) => {
113
+ res.setHeader('Access-Control-Allow-Origin', options.origin);
114
+ res.setHeader('Access-Control-Allow-Methods', options.methods);
115
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
116
+ return res;
117
+ };
118
+
119
+ // 4. Enhanced DX: Automatic Body & File Parsing
120
+ const parseBody = () => {
121
+ return new Promise((resolve) => {
122
+ if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return resolve({ body: null, files: [] });
123
+ const contentType = req.headers['content-type'] || '';
124
+
125
+ if (config.features.multipart && contentType.includes('multipart/form-data')) {
126
+ const busboy = require('busboy');
127
+ const bb = busboy({ headers: req.headers });
128
+ const body = {};
129
+ const files = [];
130
+
131
+ bb.on('file', (name, file, info) => {
132
+ const chunks = [];
133
+ file.on('data', data => chunks.push(data));
134
+ file.on('end', () => {
135
+ files.push({
136
+ fieldname: name,
137
+ filename: info.filename,
138
+ encoding: info.encoding,
139
+ mimetype: info.mimeType,
140
+ buffer: Buffer.concat(chunks)
141
+ });
142
+ });
143
+ });
144
+
145
+ bb.on('field', (name, val) => {
146
+ body[name] = val;
147
+ });
148
+
149
+ bb.on('close', () => resolve({ body, files }));
150
+ req.pipe(bb);
151
+ } else {
152
+ let rawBody = '';
153
+ req.on('data', chunk => { rawBody += chunk.toString(); });
154
+ req.on('end', () => {
155
+ try {
156
+ if (contentType.includes('application/json') && rawBody) {
157
+ resolve({ body: JSON.parse(rawBody), files: [] });
158
+ } else {
159
+ resolve({ body: rawBody, files: [] });
160
+ }
161
+ } catch (e) {
162
+ resolve({ body: {}, files: [] });
163
+ }
164
+ });
165
+ }
166
+ });
167
+ };
168
+
169
+ const parsedData = await parseBody();
170
+ req.body = parsedData.body;
171
+ req.files = parsedData.files;
172
+
173
+ // Handle OPTIONS requests automatically for CORS if requested
174
+ if (method === 'OPTIONS') {
175
+ res.cors();
176
+ res.statusCode = 204;
177
+ return res.end();
178
+ }
179
+
180
+ req.params = {};
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
+ };
20
201
 
21
- if (typeof handler === "function") {
22
- handler(req, res);
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
+
249
+ let filePath = path.join(apiDir, `${cleanPath}.js`);
250
+
251
+ // 6. Enhanced DX: Dynamic Routing ([id].js)
252
+ if (config.features.dynamicRouting && !fs.existsSync(filePath)) {
253
+ const parts = cleanPath.split('/').filter(Boolean);
254
+ let currentDir = apiDir;
255
+ let matchedFile = null;
256
+
257
+ for (let i = 0; i < parts.length; i++) {
258
+ const part = parts[i];
259
+ const isLast = i === parts.length - 1;
260
+
261
+ if (fs.existsSync(currentDir)) {
262
+ const files = fs.readdirSync(currentDir);
263
+
264
+ // Look for exact match first
265
+ let match = files.find(f => isLast ? f === `${part}.js` : f === part && fs.statSync(path.join(currentDir, f)).isDirectory());
266
+
267
+ // Look for dynamic parameter [param]
268
+ if (!match) {
269
+ match = files.find(f => isLast ? (f.startsWith('[') && f.endsWith('].js')) : (f.startsWith('[') && f.endsWith(']') && fs.statSync(path.join(currentDir, f)).isDirectory()));
270
+ if (match) {
271
+ const paramName = isLast ? match.slice(1, -4) : match.slice(1, -1);
272
+ req.params[paramName] = part;
273
+ }
274
+ }
275
+
276
+ if (match) {
277
+ if (isLast) {
278
+ matchedFile = path.join(currentDir, match);
279
+ } else {
280
+ currentDir = path.join(currentDir, match);
281
+ }
282
+ } else {
283
+ break;
284
+ }
23
285
  } else {
24
- res.statusCode = 500;
25
- res.end(`Error: Handler in ${cleanPath}.js must be a function.`);
286
+ break;
287
+ }
288
+ }
289
+
290
+ if (matchedFile) {
291
+ filePath = matchedFile;
292
+ }
293
+ }
294
+
295
+ if (fs.existsSync(filePath)) {
296
+ try {
297
+ // 7. Enhanced DX: Middleware (_middleware.js)
298
+ const targetDir = path.dirname(filePath);
299
+ let currentPath = targetDir;
300
+ const middlewarePaths = [];
301
+
302
+ if (config.features.middleware) {
303
+ while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
304
+ const mwPath = path.join(currentPath, '_middleware.js');
305
+ if (fs.existsSync(mwPath)) {
306
+ middlewarePaths.unshift(mwPath); // Run top-down
307
+ }
308
+ if (currentPath === apiDir) break;
309
+ currentPath = path.dirname(currentPath);
310
+ }
26
311
  }
312
+
313
+ let middlewareIndex = 0;
314
+ let responseSent = false;
315
+
316
+ // Track if a response was sent to avoid double execution
317
+ const originalEndM = res.end;
318
+ res.end = function (...args) {
319
+ responseSent = true;
320
+ return originalEndM.apply(this, args);
321
+ };
322
+
323
+ const runNext = async () => {
324
+ if (responseSent) return;
325
+
326
+ if (middlewareIndex < middlewarePaths.length) {
327
+ const mwPath = middlewarePaths[middlewareIndex++];
328
+ delete require.cache[require.resolve(mwPath)];
329
+ const mw = require(mwPath);
330
+ if (typeof mw === 'function') {
331
+ await mw(req, res, runNext);
332
+ } else {
333
+ await runNext();
334
+ }
335
+ } else {
336
+ delete require.cache[require.resolve(filePath)];
337
+ const handler = require(filePath);
338
+
339
+ if (typeof handler === "function" || (handler && typeof handler.default === "function")) {
340
+ const actualHandler = handler.default || handler;
341
+
342
+ // 9. Enhanced DX: Input Validation
343
+ const schema = handler.schema;
344
+ if (config.features.validation && schema && typeof req.body === 'object' && req.body !== null) {
345
+ const errors = [];
346
+ for (const [key, type] of Object.entries(schema)) {
347
+ if (typeof req.body[key] !== type) {
348
+ errors.push(`Expected '${key}' to be '${type}', got '${typeof req.body[key]}'`);
349
+ }
350
+ }
351
+ if (errors.length > 0) {
352
+ return res.status(400).json({ error: "Validation Failed", details: errors });
353
+ }
354
+ }
355
+
356
+ await actualHandler(req, res);
357
+ } else {
358
+ res.status(500).json({ error: `Handler in ${filePath} must be a function.` });
359
+ }
360
+ }
361
+ };
362
+
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();
374
+
27
375
  } catch (err) {
28
- res.statusCode = 500;
29
- res.end(`Runtime Error: ${err.message}`);
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
402
+ res.status(500).json({ error: "Runtime Error", message: err.message });
30
403
  }
31
404
  } else {
32
- res.statusCode = 404;
33
- res.end(`Route ${url} not found (No file at ${filePath})`);
405
+ res.status(404).json({ error: "Not Found", route: url });
34
406
  }
35
407
  });
36
408
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zerra-core",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -9,5 +9,8 @@
9
9
  "keywords": [],
10
10
  "author": "",
11
11
  "license": "ISC",
12
- "type": "commonjs"
12
+ "type": "commonjs",
13
+ "dependencies": {
14
+ "busboy": "^1.6.0"
15
+ }
13
16
  }