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.
- package/index.js +255 -18
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|