zerra-core 1.0.0 → 1.1.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 +255 -18
  2. package/package.json +5 -2
package/index.js CHANGED
@@ -3,34 +3,271 @@ 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
+ }
16
+ };
17
+ if (fs.existsSync(configPath)) {
18
+ try {
19
+ const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
20
+ config.features = { ...config.features, ...(userConfig.features || {}) };
21
+ } catch (e) {
22
+ console.warn("⚠️ Invalid zerra.config.json. Using defaults.");
23
+ }
24
+ }
25
+
26
+ // 8. Enhanced DX: Auto-load .env files
27
+ if (config.features.dotenv) {
28
+ const envPath = path.join(process.cwd(), '.env');
29
+ if (fs.existsSync(envPath)) {
30
+ const envFile = fs.readFileSync(envPath, 'utf8');
31
+ envFile.split('\n').forEach(line => {
32
+ const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
33
+ if (match) {
34
+ const key = match[1];
35
+ let value = match[2] || '';
36
+ // Remove quotes
37
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
38
+ else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
39
+ if (!process.env.hasOwnProperty(key)) process.env[key] = value;
40
+ }
41
+ });
42
+ }
43
+ }
44
+
6
45
  const apiDir = path.join(process.cwd(), "api");
7
46
 
8
- const server = http.createServer((req, res) => {
47
+ const server = http.createServer(async (req, res) => {
9
48
  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`);
49
+ const startTime = Date.now();
14
50
 
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);
51
+ // 1. Enhanced DX: Beautiful Request Logging
52
+ const originalEnd = res.end;
53
+ res.end = function (...args) {
54
+ if (config.features.logging) {
55
+ const duration = Date.now() - startTime;
56
+ const statusColor = res.statusCode >= 500 ? '\x1b[31m' : res.statusCode >= 400 ? '\x1b[33m' : '\x1b[32m';
57
+ const resetColor = '\x1b[0m';
58
+ console.log(`${statusColor}[${method}]${resetColor} ${req.path || url} ➜ ${statusColor}${res.statusCode}${resetColor} (${duration}ms)`);
59
+ }
60
+ return originalEnd.apply(this, args);
61
+ };
62
+
63
+ // 2. Enhanced DX: Add res.status and res.json helpers
64
+ res.status = (code) => {
65
+ res.statusCode = code;
66
+ return res;
67
+ };
68
+
69
+ res.json = (data) => {
70
+ res.setHeader("Content-Type", "application/json");
71
+ res.end(JSON.stringify(data));
72
+ };
73
+
74
+ // 2. Enhanced DX: Parse query parameters
75
+ const parsedUrl = new URL(url, `http://localhost:${port}`);
76
+ req.query = Object.fromEntries(parsedUrl.searchParams);
77
+ req.path = parsedUrl.pathname;
78
+
79
+ // 3. Enhanced DX: CORS Helper
80
+ res.cors = (options = { origin: '*', methods: 'GET,POST,PUT,DELETE,OPTIONS' }) => {
81
+ res.setHeader('Access-Control-Allow-Origin', options.origin);
82
+ res.setHeader('Access-Control-Allow-Methods', options.methods);
83
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
84
+ return res;
85
+ };
86
+
87
+ // 4. Enhanced DX: Automatic Body & File Parsing
88
+ const parseBody = () => {
89
+ return new Promise((resolve) => {
90
+ if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return resolve({ body: null, files: [] });
91
+ const contentType = req.headers['content-type'] || '';
92
+
93
+ if (config.features.multipart && contentType.includes('multipart/form-data')) {
94
+ const busboy = require('busboy');
95
+ const bb = busboy({ headers: req.headers });
96
+ const body = {};
97
+ const files = [];
98
+
99
+ bb.on('file', (name, file, info) => {
100
+ const chunks = [];
101
+ file.on('data', data => chunks.push(data));
102
+ file.on('end', () => {
103
+ files.push({
104
+ fieldname: name,
105
+ filename: info.filename,
106
+ encoding: info.encoding,
107
+ mimetype: info.mimeType,
108
+ buffer: Buffer.concat(chunks)
109
+ });
110
+ });
111
+ });
112
+
113
+ bb.on('field', (name, val) => {
114
+ body[name] = val;
115
+ });
116
+
117
+ bb.on('close', () => resolve({ body, files }));
118
+ req.pipe(bb);
119
+ } else {
120
+ let rawBody = '';
121
+ req.on('data', chunk => { rawBody += chunk.toString(); });
122
+ req.on('end', () => {
123
+ try {
124
+ if (contentType.includes('application/json') && rawBody) {
125
+ resolve({ body: JSON.parse(rawBody), files: [] });
126
+ } else {
127
+ resolve({ body: rawBody, files: [] });
128
+ }
129
+ } catch (e) {
130
+ resolve({ body: {}, files: [] });
131
+ }
132
+ });
133
+ }
134
+ });
135
+ };
136
+
137
+ const parsedData = await parseBody();
138
+ req.body = parsedData.body;
139
+ req.files = parsedData.files;
140
+
141
+ // Handle OPTIONS requests automatically for CORS if requested
142
+ if (method === 'OPTIONS') {
143
+ res.cors();
144
+ res.statusCode = 204;
145
+ return res.end();
146
+ }
147
+
148
+ req.params = {};
149
+ const cleanPath = req.path === "/" ? "/index" : req.path;
150
+ let filePath = path.join(apiDir, `${cleanPath}.js`);
151
+
152
+ // 6. Enhanced DX: Dynamic Routing ([id].js)
153
+ if (config.features.dynamicRouting && !fs.existsSync(filePath)) {
154
+ const parts = cleanPath.split('/').filter(Boolean);
155
+ let currentDir = apiDir;
156
+ let matchedFile = null;
20
157
 
21
- if (typeof handler === "function") {
22
- handler(req, res);
158
+ for (let i = 0; i < parts.length; i++) {
159
+ const part = parts[i];
160
+ const isLast = i === parts.length - 1;
161
+
162
+ if (fs.existsSync(currentDir)) {
163
+ const files = fs.readdirSync(currentDir);
164
+
165
+ // Look for exact match first
166
+ let match = files.find(f => isLast ? f === `${part}.js` : f === part && fs.statSync(path.join(currentDir, f)).isDirectory());
167
+
168
+ // Look for dynamic parameter [param]
169
+ if (!match) {
170
+ match = files.find(f => isLast ? (f.startsWith('[') && f.endsWith('].js')) : (f.startsWith('[') && f.endsWith(']') && fs.statSync(path.join(currentDir, f)).isDirectory()));
171
+ if (match) {
172
+ const paramName = isLast ? match.slice(1, -4) : match.slice(1, -1);
173
+ req.params[paramName] = part;
174
+ }
175
+ }
176
+
177
+ if (match) {
178
+ if (isLast) {
179
+ matchedFile = path.join(currentDir, match);
180
+ } else {
181
+ currentDir = path.join(currentDir, match);
182
+ }
183
+ } else {
184
+ break;
185
+ }
23
186
  } else {
24
- res.statusCode = 500;
25
- res.end(`Error: Handler in ${cleanPath}.js must be a function.`);
187
+ break;
188
+ }
189
+ }
190
+
191
+ if (matchedFile) {
192
+ filePath = matchedFile;
193
+ }
194
+ }
195
+
196
+ if (fs.existsSync(filePath)) {
197
+ try {
198
+ // 7. Enhanced DX: Middleware (_middleware.js)
199
+ const targetDir = path.dirname(filePath);
200
+ let currentPath = targetDir;
201
+ const middlewarePaths = [];
202
+
203
+ if (config.features.middleware) {
204
+ while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
205
+ const mwPath = path.join(currentPath, '_middleware.js');
206
+ if (fs.existsSync(mwPath)) {
207
+ middlewarePaths.unshift(mwPath); // Run top-down
208
+ }
209
+ if (currentPath === apiDir) break;
210
+ currentPath = path.dirname(currentPath);
211
+ }
26
212
  }
213
+
214
+ let middlewareIndex = 0;
215
+ let responseSent = false;
216
+
217
+ // Track if a response was sent to avoid double execution
218
+ const originalEndM = res.end;
219
+ res.end = function (...args) {
220
+ responseSent = true;
221
+ return originalEndM.apply(this, args);
222
+ };
223
+
224
+ const runNext = async () => {
225
+ if (responseSent) return;
226
+
227
+ if (middlewareIndex < middlewarePaths.length) {
228
+ const mwPath = middlewarePaths[middlewareIndex++];
229
+ delete require.cache[require.resolve(mwPath)];
230
+ const mw = require(mwPath);
231
+ if (typeof mw === 'function') {
232
+ await mw(req, res, runNext);
233
+ } else {
234
+ await runNext();
235
+ }
236
+ } else {
237
+ delete require.cache[require.resolve(filePath)];
238
+ const handler = require(filePath);
239
+
240
+ if (typeof handler === "function" || (handler && typeof handler.default === "function")) {
241
+ const actualHandler = handler.default || handler;
242
+
243
+ // 9. Enhanced DX: Input Validation
244
+ const schema = handler.schema;
245
+ if (config.features.validation && schema && typeof req.body === 'object' && req.body !== null) {
246
+ const errors = [];
247
+ for (const [key, type] of Object.entries(schema)) {
248
+ if (typeof req.body[key] !== type) {
249
+ errors.push(`Expected '${key}' to be '${type}', got '${typeof req.body[key]}'`);
250
+ }
251
+ }
252
+ if (errors.length > 0) {
253
+ return res.status(400).json({ error: "Validation Failed", details: errors });
254
+ }
255
+ }
256
+
257
+ await actualHandler(req, res);
258
+ } else {
259
+ res.status(500).json({ error: `Handler in ${filePath} must be a function.` });
260
+ }
261
+ }
262
+ };
263
+
264
+ await runNext();
265
+
27
266
  } catch (err) {
28
- res.statusCode = 500;
29
- res.end(`Runtime Error: ${err.message}`);
267
+ res.status(500).json({ error: "Runtime Error", message: err.message });
30
268
  }
31
269
  } else {
32
- res.statusCode = 404;
33
- res.end(`Route ${url} not found (No file at ${filePath})`);
270
+ res.status(404).json({ error: "Not Found", route: url });
34
271
  }
35
272
  });
36
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zerra-core",
3
- "version": "1.0.0",
3
+ "version": "1.1.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
  }