lieko-express 0.0.6 → 0.0.8

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/lieko-express.js CHANGED
@@ -1,410 +1,17 @@
1
1
  const { createServer } = require('http');
2
2
  const net = require("net");
3
+ const fs = require("fs");
4
+ const path = require("path");
3
5
 
4
- process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
5
-
6
- class ValidationError extends Error {
7
- constructor(errors) {
8
- super('Validation failed');
9
- this.name = 'ValidationError';
10
- this.errors = errors;
11
- }
12
- }
13
-
14
- class Schema {
15
- constructor(rules) {
16
- this.rules = rules;
17
- this.fields = rules;
18
- }
19
-
20
- validate(data) {
21
- const errors = [];
22
- for (const [field, validators] of Object.entries(this.rules)) {
23
- const value = data[field];
24
- for (const validator of validators) {
25
- const error = validator(value, field, data);
26
- if (error) {
27
- errors.push(error);
28
- break;
29
- }
30
- }
31
- }
32
- if (errors.length > 0) throw new ValidationError(errors);
33
- return true;
34
- }
35
- }
36
-
37
- const validators = {
38
- required: (message = 'Field is required') => {
39
- return (value, field) => {
40
- if (value === undefined || value === null || value === '') {
41
- return { field, message, type: 'required' };
42
- }
43
- return null;
44
- };
45
- },
46
-
47
- requiredTrue: (message = 'Field must be true') => {
48
- return (value, field) => {
49
- const normalized = value === true || value === 'true' || value === '1' || value === 1;
50
- if (!normalized) {
51
- return { field, message, type: 'requiredTrue' };
52
- }
53
- return null;
54
- }
55
- },
56
-
57
- optional: () => {
58
- return () => null;
59
- },
60
-
61
- string: (message = 'Field must be a string') => {
62
- return (value, field) => {
63
- if (value !== undefined && typeof value !== 'string') {
64
- return { field, message, type: 'string' };
65
- }
66
- return null;
67
- };
68
- },
69
-
70
- number: (message = 'Field must be a number') => {
71
- return (value, field) => {
72
- if (value !== undefined && typeof value !== 'number') {
73
- return { field, message, type: 'number' };
74
- }
75
- return null;
76
- };
77
- },
78
-
79
- boolean: (message = 'Field must be a boolean') => {
80
- return (value, field) => {
81
- if (value === undefined || value === null || value === '') return null;
82
-
83
- const validTrue = ['true', true, 1, '1'];
84
- const validFalse = ['false', false, 0, '0'];
85
-
86
- const isValid = validTrue.includes(value) || validFalse.includes(value);
87
-
88
- if (!isValid) {
89
- return { field, message, type: 'boolean' };
90
- }
91
-
92
- return null;
93
- };
94
- },
95
-
96
- integer: (message = 'Field must be an integer') => {
97
- return (value, field) => {
98
- if (value !== undefined && !Number.isInteger(value)) {
99
- return { field, message, type: 'integer' };
100
- }
101
- return null;
102
- };
103
- },
104
-
105
- positive: (message = 'Field must be positive') => {
106
- return (value, field) => {
107
- if (value !== undefined && value <= 0) {
108
- return { field, message, type: 'positive' };
109
- }
110
- return null;
111
- };
112
- },
113
-
114
- negative: (message = 'Field must be negative') => {
115
- return (value, field) => {
116
- if (value !== undefined && value >= 0) {
117
- return { field, message, type: 'negative' };
118
- }
119
- return null;
120
- };
121
- },
122
-
123
- email: (message = 'Invalid email format') => {
124
- return (value, field) => {
125
- if (value !== undefined && value !== null) {
126
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
127
- if (!emailRegex.test(value)) {
128
- return { field, message, type: 'email' };
129
- }
130
- }
131
- return null;
132
- };
133
- },
134
-
135
- min: (minValue, message) => {
136
- return (value, field) => {
137
- if (value !== undefined && value !== null) {
138
- if (typeof value === 'string' && value.length < minValue) {
139
- return {
140
- field,
141
- message: message || `Field must be at least ${minValue} characters`,
142
- type: 'min'
143
- };
144
- }
145
- if (typeof value === 'number' && value < minValue) {
146
- return {
147
- field,
148
- message: message || `Field must be at least ${minValue}`,
149
- type: 'min'
150
- };
151
- }
152
- }
153
- return null;
154
- };
155
- },
156
-
157
- max: (maxValue, message) => {
158
- return (value, field) => {
159
- if (value !== undefined && value !== null) {
160
- if (typeof value === 'string' && value.length > maxValue) {
161
- return {
162
- field,
163
- message: message || `Field must be at most ${maxValue} characters`,
164
- type: 'max'
165
- };
166
- }
167
- if (typeof value === 'number' && value > maxValue) {
168
- return {
169
- field,
170
- message: message || `Field must be at most ${maxValue}`,
171
- type: 'max'
172
- };
173
- }
174
- }
175
- return null;
176
- };
177
- },
178
-
179
- length: (n, message) => {
180
- return (value, field) => {
181
- if (typeof value === 'string' && value.length !== n) {
182
- return {
183
- field,
184
- message: message || `Field must be exactly ${n} characters`,
185
- type: 'length'
186
- };
187
- }
188
- return null;
189
- };
190
- },
191
-
192
- minLength: (minLength, message) => {
193
- return (value, field) => {
194
- if (value !== undefined && value !== null && typeof value === 'string') {
195
- if (value.length < minLength) {
196
- return {
197
- field,
198
- message: message || `Field must be at least ${minLength} characters`,
199
- type: 'minLength'
200
- };
201
- }
202
- }
203
- return null;
204
- };
205
- },
206
-
207
- maxLength: (maxLength, message) => {
208
- return (value, field) => {
209
- if (value !== undefined && value !== null && typeof value === 'string') {
210
- if (value.length > maxLength) {
211
- return {
212
- field,
213
- message: message || `Field must be at most ${maxLength} characters`,
214
- type: 'maxLength'
215
- };
216
- }
217
- }
218
- return null;
219
- };
220
- },
221
-
222
- pattern: (regex, message = 'Invalid format') => {
223
- return (value, field) => {
224
- if (value !== undefined && value !== null && typeof value === 'string') {
225
- if (!regex.test(value)) {
226
- return { field, message, type: 'pattern' };
227
- }
228
- }
229
- return null;
230
- };
231
- },
232
-
233
- oneOf: (allowedValues, message) => {
234
- return (value, field) => {
235
- if (value !== undefined && value !== null) {
236
- if (!allowedValues.includes(value)) {
237
- return {
238
- field,
239
- message: message || `Field must be one of: ${allowedValues.join(', ')}`,
240
- type: 'oneOf'
241
- };
242
- }
243
- }
244
- return null;
245
- };
246
- },
247
-
248
- notOneOf: (values, message) => {
249
- return (value, field) => {
250
- if (values.includes(value)) {
251
- return {
252
- field,
253
- message: message || `Field cannot be one of: ${values.join(', ')}`,
254
- type: 'notOneOf'
255
- };
256
- }
257
- return null;
258
- };
259
- },
260
-
261
- custom: (validatorFn, message = 'Validation failed') => {
262
- return (value, field, data) => {
263
- const isValid = validatorFn(value, data);
264
- if (!isValid) {
265
- return { field, message, type: 'custom' };
266
- }
267
- return null;
268
- };
269
- },
270
-
271
- equal: (expectedValue, message) => {
272
- return (value, field) => {
273
- if (value !== expectedValue) {
274
- return {
275
- field,
276
- message: message || `Field must be equal to ${expectedValue}`,
277
- type: 'equal'
278
- };
279
- }
280
- return null;
281
- };
282
- },
283
-
284
- mustBeTrue: (message = 'This field must be accepted') => {
285
- return (value, field) => {
286
- const normalized = value === true || value === 'true' || value === '1' || value === 1;
287
- if (!normalized) {
288
- return { field, message, type: 'mustBeTrue' };
289
- }
290
- return null;
291
- };
292
- },
293
-
294
- mustBeFalse: (message = 'This field must be declined') => {
295
- return (value, field) => {
296
- const normalized = value === false || value === 'false' || value === '0' || value === 0;
297
- if (!normalized) {
298
- return { field, message, type: 'mustBeFalse' };
299
- }
300
- return null;
301
- };
302
- },
303
-
304
- date: (message = 'Invalid date') => {
305
- return (value, field) => {
306
- if (!value) return null;
307
- const date = new Date(value);
308
- if (isNaN(date.getTime())) {
309
- return { field, message, type: 'date' };
310
- }
311
- return null;
312
- };
313
- },
314
-
315
- before: (limit, message) => {
316
- return (value, field) => {
317
- if (!value) return null;
318
- const d1 = new Date(value);
319
- const d2 = new Date(limit);
320
- if (isNaN(d1) || d1 >= d2) {
321
- return {
322
- field,
323
- message: message || `Date must be before ${limit}`,
324
- type: 'before'
325
- };
326
- }
327
- return null;
328
- };
329
- },
330
-
331
- after: (limit, message) => {
332
- return (value, field) => {
333
- if (!value) return null;
334
- const d1 = new Date(value);
335
- const d2 = new Date(limit);
336
- if (isNaN(d1) || d1 <= d2) {
337
- return {
338
- field,
339
- message: message || `Date must be after ${limit}`,
340
- type: 'after'
341
- };
342
- }
343
- return null;
344
- };
345
- },
346
-
347
- startsWith: (prefix, message) => {
348
- return (value, field) => {
349
- if (typeof value === 'string' && !value.startsWith(prefix)) {
350
- return {
351
- field,
352
- message: message || `Field must start with "${prefix}"`,
353
- type: 'startsWith'
354
- };
355
- }
356
- return null;
357
- };
358
- },
359
-
360
- endsWith: (suffix, message) => {
361
- return (value, field) => {
362
- if (typeof value === 'string' && !value.endsWith(suffix)) {
363
- return {
364
- field,
365
- message: message || `Field must end with "${suffix}"`,
366
- type: 'endsWith'
367
- };
368
- }
369
- return null;
370
- };
371
- }
372
- };
6
+ const {
7
+ Schema,
8
+ ValidationError,
9
+ validators,
10
+ validate,
11
+ validatePartial
12
+ } = require('./lib/schema');
373
13
 
374
- function validate(schema) {
375
- return (req, res, next) => {
376
- try {
377
- schema.validate(req.body);
378
- next();
379
- } catch (error) {
380
- if (error instanceof ValidationError) {
381
- return res.status(400).json({
382
- success: false,
383
- message: 'Validation failed',
384
- errors: error.errors
385
- });
386
- }
387
- throw error;
388
- }
389
- };
390
- }
391
-
392
- function validatePartial(schema) {
393
- const partial = {};
394
-
395
- for (const field in schema.fields) {
396
- const rules = schema.fields[field];
397
-
398
- const filtered = rules.filter(v =>
399
- v.name !== 'required' &&
400
- v.name !== 'requiredTrue' &&
401
- v.name !== 'mustBeTrue'
402
- );
403
- partial[field] = [validators.optional(), ...filtered];
404
- }
405
-
406
- return new Schema(partial);
407
- }
14
+ process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
408
15
 
409
16
  class LiekoExpress {
410
17
  constructor() {
@@ -420,9 +27,14 @@ class LiekoExpress {
420
27
  'x-powered-by': 'lieko-express',
421
28
  'trust proxy': false,
422
29
  strictTrailingSlash: true,
423
- allowTrailingSlash: false,
30
+ allowTrailingSlash: true,
31
+ views: path.join(process.cwd(), "views"),
32
+ "view engine": "html"
424
33
  };
425
34
 
35
+ this.engines = {};
36
+ this.engines['.html'] = this._defaultHtmlEngine.bind(this);
37
+
426
38
  this.bodyParserOptions = {
427
39
  json: {
428
40
  limit: '10mb',
@@ -545,13 +157,50 @@ class LiekoExpress {
545
157
  }
546
158
  }
547
159
 
160
+ _logCorsDebug(req, opts) {
161
+ if (!opts.debug) return;
162
+
163
+ console.log("\n[CORS DEBUG]");
164
+ console.log("Request:", req.method, req.url);
165
+ console.log("Origin:", req.headers.origin || "none");
166
+
167
+ console.log("Applied CORS Policy:");
168
+ console.log(" - Access-Control-Allow-Origin:", opts.origin);
169
+ console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
170
+ console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
171
+
172
+ if (opts.credentials) {
173
+ console.log(" - Access-Control-Allow-Credentials: true");
174
+ }
175
+
176
+ if (opts.exposedHeaders?.length) {
177
+ console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
178
+ }
179
+
180
+ console.log(" - Max-Age:", opts.maxAge);
181
+
182
+ if (req.method === "OPTIONS") {
183
+ console.log("Preflight request handled with status 204\n");
184
+ }
185
+ }
186
+
187
+ debug(value = true) {
188
+ if (typeof value === 'string') {
189
+ value = value.toLowerCase() === 'true';
190
+ }
191
+ this.set('debug', value);
192
+ return this;
193
+ }
194
+
548
195
  set(name, value) {
549
196
  this.settings[name] = value;
550
197
  return this;
551
198
  }
552
199
 
553
- get(name) {
554
- return this.settings[name];
200
+ engine(ext, renderFunction) {
201
+ if (!ext.startsWith(".")) ext = "." + ext;
202
+ this.engines[ext] = renderFunction;
203
+ return this;
555
204
  }
556
205
 
557
206
  enable(name) {
@@ -576,6 +225,7 @@ class LiekoExpress {
576
225
  if (options.limit) {
577
226
  this.bodyParserOptions.json.limit = options.limit;
578
227
  this.bodyParserOptions.urlencoded.limit = options.limit;
228
+ this.bodyParserOptions.multipart.limit = options.limit;
579
229
  }
580
230
  if (options.extended !== undefined) {
581
231
  this.bodyParserOptions.urlencoded.extended = options.extended;
@@ -617,7 +267,7 @@ class LiekoExpress {
617
267
  if (typeof limit === 'number') return limit;
618
268
 
619
269
  const match = limit.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
620
- if (!match) return 1048576; // 1mb par défaut
270
+ if (!match) return 1048576;
621
271
 
622
272
  const value = parseFloat(match[1]);
623
273
  const unit = (match[2] || 'b').toLowerCase();
@@ -810,9 +460,13 @@ class LiekoExpress {
810
460
  });
811
461
  }
812
462
 
813
- get(path, ...handlers) {
814
- this._addRoute('GET', path, ...handlers);
815
- return this;
463
+ get(...args) {
464
+ if (args.length === 1 && typeof args[0] === 'string' && !args[0].startsWith('/')) {
465
+ return this.settings[args[0]];
466
+ } else {
467
+ this._addRoute('GET', ...args);
468
+ return this;
469
+ }
816
470
  }
817
471
 
818
472
  post(path, ...handlers) {
@@ -867,6 +521,12 @@ class LiekoExpress {
867
521
  all(path, ...handlers) { return this._call('all', path, handlers); },
868
522
 
869
523
  use(pathOrMw, ...rest) {
524
+ if (typeof pathOrMw === 'object' && pathOrMw instanceof LiekoExpress) {
525
+ const finalPath = fullBase === '/' ? '/' : fullBase;
526
+ parent.use(finalPath, ...middlewares, pathOrMw);
527
+ return subApp;
528
+ }
529
+
870
530
  if (typeof pathOrMw === "function") {
871
531
  parent.use(fullBase, ...middlewares, pathOrMw);
872
532
  return subApp;
@@ -904,16 +564,66 @@ class LiekoExpress {
904
564
  if (isAsync) return;
905
565
 
906
566
  if (handler.length < 3) {
907
- console.warn(`
908
- ⚠️ WARNING: Middleware executed without a 'next' parameter.
909
- This middleware may block the request pipeline.
567
+ const funcString = handler.toString();
568
+ const stack = new Error().stack;
569
+ let userFileInfo = 'unknown location';
570
+ let userLine = '';
571
+
572
+ if (stack) {
573
+ const lines = stack.split('\n');
574
+
575
+ for (let i = 0; i < lines.length; i++) {
576
+ const line = lines[i].trim();
577
+
578
+ if (line.includes('lieko-express.js') ||
579
+ line.includes('_checkMiddleware') ||
580
+ line.includes('at LiekoExpress.') ||
581
+ line.includes('at Object.<anonymous>') ||
582
+ line.includes('at Module._compile')) {
583
+ continue;
584
+ }
585
+
586
+ const fileMatch = line.match(/\(?(.+?):(\d+):(\d+)\)?$/);
587
+ if (fileMatch) {
588
+ const filePath = fileMatch[1];
589
+ const lineNumber = fileMatch[2];
590
+
591
+ const shortPath = filePath.replace(process.cwd(), '.');
592
+ userFileInfo = `${shortPath}:${lineNumber}`;
593
+ userLine = line;
594
+ break;
595
+ }
596
+ }
597
+ }
598
+
599
+ const firstLine = funcString.split('\n')[0];
600
+ const secondLine = funcString.split('\n')[1] || '';
601
+ const thirdLine = funcString.split('\n')[2] || '';
910
602
 
911
- Offending middleware:
912
- ${handler.toString().split('\n')[0].substring(0, 120)}...
603
+ const yellow = '\x1b[33m';
604
+ const red = '\x1b[31m';
605
+ const cyan = '\x1b[36m';
606
+ const reset = '\x1b[0m';
607
+ const bold = '\x1b[1m';
913
608
 
914
- Fix: Add 'next' as third parameter and call it:
915
- (req, res, next) => { /* your code */ next(); }
916
- `);
609
+ console.warn(`
610
+ ${yellow}${bold}⚠️ WARNING: Middleware missing 'next' parameter${reset}
611
+ ${yellow}This middleware may block the request pipeline.${reset}
612
+
613
+ ${cyan}📍 Defined at:${reset} ${userFileInfo}
614
+ ${userLine ? `${cyan} Stack trace:${reset} ${userLine}` : ''}
615
+
616
+ ${cyan}🔧 Middleware definition:${reset}
617
+ ${yellow}${firstLine.substring(0, 100)}${firstLine.length > 100 ? '...' : ''}${reset}
618
+ ${secondLine ? `${yellow} ${secondLine.substring(0, 100)}${secondLine.length > 100 ? '...' : ''}${reset}` : ''}
619
+ ${thirdLine ? `${yellow} ${thirdLine.substring(0, 100)}${thirdLine.length > 100 ? '...' : ''}${reset}` : ''}
620
+
621
+ ${red}${bold}FIX:${reset} Add 'next' as third parameter and call it:
622
+ ${cyan} (req, res, next) => {
623
+ // your code here
624
+ next(); // ← Don't forget to call next()
625
+ }${reset}
626
+ `);
917
627
  }
918
628
  }
919
629
 
@@ -997,12 +707,257 @@ class LiekoExpress {
997
707
  throw new Error('Invalid use() arguments');
998
708
  }
999
709
 
710
+ static(root, options = {}) {
711
+ const opts = {
712
+ maxAge: options.maxAge || 0,
713
+ index: options.index !== undefined ? options.index : 'index.html',
714
+ dotfiles: options.dotfiles || 'ignore',
715
+ etag: options.etag !== undefined ? options.etag : true,
716
+ extensions: options.extensions || false,
717
+ fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
718
+ immutable: options.immutable || false,
719
+ lastModified: options.lastModified !== undefined ? options.lastModified : true,
720
+ redirect: options.redirect !== undefined ? options.redirect : true,
721
+ setHeaders: options.setHeaders || null,
722
+ cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
723
+ };
724
+
725
+ const mimeTypes = {
726
+ '.html': 'text/html; charset=utf-8',
727
+ '.htm': 'text/html; charset=utf-8',
728
+ '.css': 'text/css; charset=utf-8',
729
+ '.js': 'application/javascript; charset=utf-8',
730
+ '.mjs': 'application/javascript; charset=utf-8',
731
+ '.json': 'application/json; charset=utf-8',
732
+ '.xml': 'application/xml; charset=utf-8',
733
+ '.txt': 'text/plain; charset=utf-8',
734
+ '.md': 'text/markdown; charset=utf-8',
735
+ '.jpg': 'image/jpeg',
736
+ '.jpeg': 'image/jpeg',
737
+ '.png': 'image/png',
738
+ '.gif': 'image/gif',
739
+ '.svg': 'image/svg+xml',
740
+ '.webp': 'image/webp',
741
+ '.ico': 'image/x-icon',
742
+ '.bmp': 'image/bmp',
743
+ '.tiff': 'image/tiff',
744
+ '.tif': 'image/tiff',
745
+ '.mp3': 'audio/mpeg',
746
+ '.wav': 'audio/wav',
747
+ '.ogg': 'audio/ogg',
748
+ '.m4a': 'audio/mp4',
749
+ '.aac': 'audio/aac',
750
+ '.flac': 'audio/flac',
751
+ '.mp4': 'video/mp4',
752
+ '.webm': 'video/webm',
753
+ '.ogv': 'video/ogg',
754
+ '.avi': 'video/x-msvideo',
755
+ '.mov': 'video/quicktime',
756
+ '.wmv': 'video/x-ms-wmv',
757
+ '.flv': 'video/x-flv',
758
+ '.mkv': 'video/x-matroska',
759
+ '.woff': 'font/woff',
760
+ '.woff2': 'font/woff2',
761
+ '.ttf': 'font/ttf',
762
+ '.otf': 'font/otf',
763
+ '.eot': 'application/vnd.ms-fontobject',
764
+ '.zip': 'application/zip',
765
+ '.rar': 'application/x-rar-compressed',
766
+ '.tar': 'application/x-tar',
767
+ '.gz': 'application/gzip',
768
+ '.7z': 'application/x-7z-compressed',
769
+ '.pdf': 'application/pdf',
770
+ '.doc': 'application/msword',
771
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
772
+ '.xls': 'application/vnd.ms-excel',
773
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
774
+ '.ppt': 'application/vnd.ms-powerpoint',
775
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
776
+ '.wasm': 'application/wasm',
777
+ '.csv': 'text/csv; charset=utf-8'
778
+ };
779
+
780
+ const getMimeType = (filePath) => {
781
+ const ext = path.extname(filePath).toLowerCase();
782
+ return mimeTypes[ext] || 'application/octet-stream';
783
+ };
784
+
785
+ const generateETag = (stats) => {
786
+ const mtime = stats.mtime.getTime().toString(16);
787
+ const size = stats.size.toString(16);
788
+ return `W/"${size}-${mtime}"`;
789
+ };
790
+
791
+ return async (req, res, next) => {
792
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
793
+ return next();
794
+ }
795
+
796
+ try {
797
+ let pathname = req.url;
798
+ const qIndex = pathname.indexOf('?');
799
+ if (qIndex !== -1) {
800
+ pathname = pathname.substring(0, qIndex);
801
+ }
802
+
803
+ if (pathname === '') {
804
+ pathname = '/';
805
+ }
806
+
807
+ try {
808
+ pathname = decodeURIComponent(pathname);
809
+ } catch (e) {
810
+ if (opts.fallthrough) return next();
811
+ return res.status(400).send('Bad Request');
812
+ }
813
+
814
+ let filePath = pathname === '/' ? root : path.join(root, pathname);
815
+
816
+ const resolvedPath = path.resolve(filePath);
817
+ const resolvedRoot = path.resolve(root);
818
+
819
+ if (!resolvedPath.startsWith(resolvedRoot)) {
820
+ if (opts.fallthrough) return next();
821
+ return res.status(403).send('Forbidden');
822
+ }
823
+
824
+ let stats;
825
+ try {
826
+ stats = await fs.promises.stat(filePath);
827
+ } catch (err) {
828
+ if (pathname === '/' && opts.index) {
829
+ const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
830
+ for (const indexFile of indexes) {
831
+ const indexPath = path.join(root, indexFile);
832
+ try {
833
+ stats = await fs.promises.stat(indexPath);
834
+ if (stats.isFile()) {
835
+ filePath = indexPath;
836
+ break;
837
+ }
838
+ } catch (e) { }
839
+ }
840
+ }
841
+
842
+ if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
843
+ let found = false;
844
+ for (const ext of opts.extensions) {
845
+ const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
846
+ try {
847
+ stats = await fs.promises.stat(testPath);
848
+ filePath = testPath;
849
+ found = true;
850
+ break;
851
+ } catch (e) { }
852
+ }
853
+ if (!found) return next();
854
+ } else if (!stats) {
855
+ return next();
856
+ }
857
+ }
858
+
859
+ if (stats.isDirectory()) {
860
+ if (opts.redirect && !pathname.endsWith('/')) {
861
+ const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
862
+ const redirectUrl = pathname + '/' + query;
863
+ return res.redirect(redirectUrl, 301);
864
+ }
865
+
866
+ if (opts.index) {
867
+ const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
868
+
869
+ for (const indexFile of indexes) {
870
+ const indexPath = path.join(filePath, indexFile);
871
+ try {
872
+ const indexStats = await fs.promises.stat(indexPath);
873
+ if (indexStats.isFile()) {
874
+ filePath = indexPath;
875
+ stats = indexStats;
876
+ break;
877
+ }
878
+ } catch (e) { }
879
+ }
880
+
881
+ if (stats.isDirectory()) {
882
+ if (opts.fallthrough) return next();
883
+ return res.status(404).send('Not Found');
884
+ }
885
+ } else {
886
+ if (opts.fallthrough) return next();
887
+ return res.status(404).send('Not Found');
888
+ }
889
+ }
890
+
891
+ if (opts.etag) {
892
+ const etag = generateETag(stats);
893
+ const ifNoneMatch = req.headers['if-none-match'];
894
+
895
+ if (ifNoneMatch === etag) {
896
+ res.statusCode = 304;
897
+ res.end();
898
+ return;
899
+ }
900
+
901
+ res.setHeader('ETag', etag);
902
+ }
903
+
904
+ if (opts.lastModified) {
905
+ const lastModified = stats.mtime.toUTCString();
906
+ const ifModifiedSince = req.headers['if-modified-since'];
907
+
908
+ if (ifModifiedSince === lastModified) {
909
+ res.statusCode = 304;
910
+ res.end();
911
+ return;
912
+ }
913
+
914
+ res.setHeader('Last-Modified', lastModified);
915
+ }
916
+
917
+ if (opts.cacheControl) {
918
+ let cacheControl = 'public';
919
+
920
+ if (opts.maxAge > 0) {
921
+ cacheControl += `, max-age=${opts.maxAge}`;
922
+ }
923
+
924
+ if (opts.immutable) {
925
+ cacheControl += ', immutable';
926
+ }
927
+
928
+ res.setHeader('Cache-Control', cacheControl);
929
+ }
930
+
931
+ const mimeType = getMimeType(filePath);
932
+ res.setHeader('Content-Type', mimeType);
933
+ res.setHeader('Content-Length', stats.size);
934
+
935
+ if (typeof opts.setHeaders === 'function') {
936
+ opts.setHeaders(res, filePath, stats);
937
+ }
938
+
939
+ if (req.method === 'HEAD') {
940
+ return res.end();
941
+ }
942
+
943
+ const data = await fs.promises.readFile(filePath);
944
+ res.end(data);
945
+ return;
946
+
947
+ } catch (error) {
948
+ console.error('Static middleware error:', error);
949
+ if (opts.fallthrough) return next();
950
+ res.status(500).send('Internal Server Error');
951
+ }
952
+ };
953
+ }
954
+
1000
955
  _mountRouter(basePath, router) {
1001
956
  basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
1002
957
  router.groupStack = [...this.groupStack];
1003
958
 
1004
959
  router.routes.forEach(route => {
1005
- const fullPath = route.path === '/' ? basePath : basePath + route.path;
960
+ const fullPath = route.path === '' ? basePath : basePath + route.path;
1006
961
 
1007
962
  this.routes.push({
1008
963
  ...route,
@@ -1015,6 +970,13 @@ class LiekoExpress {
1015
970
  bodyParserOptions: router.bodyParserOptions
1016
971
  });
1017
972
  });
973
+
974
+ router.middlewares.forEach(mw => {
975
+ this.middlewares.push({
976
+ path: basePath === '' ? mw.path : (mw.path ? basePath + mw.path : basePath),
977
+ handler: mw.handler
978
+ });
979
+ });
1018
980
  }
1019
981
 
1020
982
  _addRoute(method, path, ...handlers) {
@@ -1023,6 +985,10 @@ class LiekoExpress {
1023
985
  }
1024
986
 
1025
987
  const finalHandler = handlers[handlers.length - 1];
988
+ if (!finalHandler) {
989
+ throw new Error(`Route handler is undefined for ${method} ${path}`);
990
+ }
991
+
1026
992
  const routeMiddlewares = handlers.slice(0, -1);
1027
993
 
1028
994
  routeMiddlewares.forEach(mw => {
@@ -1031,41 +997,91 @@ class LiekoExpress {
1031
997
  }
1032
998
  });
1033
999
 
1034
- this.routes.push({
1035
- method,
1036
- path,
1037
- handler: finalHandler,
1038
- middlewares: routeMiddlewares,
1039
- pattern: this._pathToRegex(path),
1040
- groupChain: [...this.groupStack]
1000
+ const paths = Array.isArray(path) ? path : [path];
1001
+
1002
+ paths.forEach(original => {
1003
+ let p = String(original).trim();
1004
+ p = p.replace(/\/+/g, '/');
1005
+
1006
+ if (p !== '/' && p.endsWith('/')) {
1007
+ p = p.slice(0, -1);
1008
+ }
1009
+
1010
+ const exists = this.routes.some(r =>
1011
+ r.method === method &&
1012
+ r.path === p &&
1013
+ r.handler === finalHandler
1014
+ );
1015
+
1016
+ if (exists) return;
1017
+
1018
+ this.routes.push({
1019
+ method,
1020
+ path: p,
1021
+ originalPath: original,
1022
+ handler: finalHandler,
1023
+ handlerName: (finalHandler && finalHandler.name) || 'anonymous',
1024
+ middlewares: routeMiddlewares,
1025
+ pattern: this._pathToRegex(p),
1026
+ allowTrailingSlash: this.settings.allowTrailingSlash ?? false,
1027
+ groupChain: [...this.groupStack]
1028
+ });
1041
1029
  });
1042
1030
  }
1043
1031
 
1044
1032
  _pathToRegex(path) {
1045
- let pattern = path
1046
- .replace(/:(\w+)/g, '(?<$1>[^/]+)') // params :id
1047
- .replace(/\*/g, '.*'); // wildcards
1033
+ let p = String(path).trim();
1034
+ p = p.replace(/\/+/g, '/');
1035
+
1036
+ if (p !== '/' && p.endsWith('/')) {
1037
+ p = p.slice(0, -1);
1038
+ }
1048
1039
 
1049
- const allowTrailing = this.settings.allowTrailingSlash === true ||
1050
- this.settings.strictTrailingSlash === false;
1040
+ let pattern = p
1041
+ .replace(/:(\w+)/g, '(?<$1>[^/]+)')
1042
+ .replace(/\*/g, '.*');
1051
1043
 
1052
- const isStaticRoute = !pattern.includes('(') &&
1053
- !pattern.includes('*') &&
1054
- !path.endsWith('/');
1044
+ const isStatic = !/[:*]/.test(p) && p !== '/';
1055
1045
 
1056
- if (allowTrailing && isStaticRoute) {
1046
+ const allowTrailing = this.settings.allowTrailingSlash !== false;
1047
+
1048
+ if (isStatic && allowTrailing) {
1057
1049
  pattern += '/?';
1058
1050
  }
1059
1051
 
1052
+ if (p === '/') {
1053
+ return /^\/?$/;
1054
+ }
1055
+
1060
1056
  return new RegExp(`^${pattern}$`);
1061
1057
  }
1062
1058
 
1063
1059
  _findRoute(method, pathname) {
1064
1060
  for (const route of this.routes) {
1065
1061
  if (route.method !== method && route.method !== 'ALL') continue;
1062
+
1066
1063
  const match = pathname.match(route.pattern);
1067
1064
  if (match) {
1068
- return { ...route, params: match.groups || {} };
1065
+ return { ...route, params: match.groups || {}, matchedPath: pathname };
1066
+ }
1067
+ }
1068
+
1069
+ if (pathname.endsWith('/') && pathname.length > 1) {
1070
+ const cleanPath = pathname.slice(0, -1);
1071
+ for (const route of this.routes) {
1072
+ if (route.method !== method && route.method !== 'ALL') continue;
1073
+
1074
+ if (route.path === cleanPath && route.allowTrailingSlash !== false) {
1075
+ const match = cleanPath.match(route.pattern);
1076
+ if (match) {
1077
+ return {
1078
+ ...route,
1079
+ params: match.groups || {},
1080
+ matchedPath: cleanPath,
1081
+ wasTrailingSlash: true
1082
+ };
1083
+ }
1084
+ }
1069
1085
  }
1070
1086
  }
1071
1087
  return null;
@@ -1160,15 +1176,21 @@ class LiekoExpress {
1160
1176
 
1161
1177
  _parseIp(rawIp) {
1162
1178
  if (!rawIp) return { raw: null, ipv4: null, ipv6: null };
1179
+ let ip = rawIp.trim();
1163
1180
 
1164
- let ip = rawIp;
1181
+ if (ip === '::1') {
1182
+ ip = '127.0.0.1';
1183
+ }
1165
1184
 
1166
- // Remove IPv6 IPv4-mapped prefix "::ffff:"
1167
- if (ip.startsWith("::ffff:")) {
1168
- ip = ip.replace("::ffff:", "");
1185
+ if (ip.startsWith('::ffff:')) {
1186
+ ip = ip.slice(7);
1169
1187
  }
1170
1188
 
1171
- const family = net.isIP(ip); // 0=invalid, 4=IPv4, 6=IPv6
1189
+ const family = net.isIP(ip);
1190
+
1191
+ if (family === 0) {
1192
+ return { raw: rawIp, ipv4: null, ipv6: null };
1193
+ }
1172
1194
 
1173
1195
  return {
1174
1196
  raw: rawIp,
@@ -1229,8 +1251,9 @@ class LiekoExpress {
1229
1251
  req._startTime = process.hrtime.bigint();
1230
1252
  this._enhanceResponse(req, res);
1231
1253
 
1232
- try {
1254
+ req.originalUrl = url;
1233
1255
 
1256
+ try {
1234
1257
  if (req.method === "OPTIONS" && this.corsOptions.enabled) {
1235
1258
  this._applyCors(req, res, this.corsOptions);
1236
1259
  return;
@@ -1239,10 +1262,8 @@ class LiekoExpress {
1239
1262
  const route = this._findRoute(req.method, pathname);
1240
1263
 
1241
1264
  if (route) {
1242
-
1243
- if (route.cors === false) { }
1244
-
1245
- else if (route.cors) {
1265
+ if (route.cors === false) {
1266
+ } else if (route.cors) {
1246
1267
  const finalCors = {
1247
1268
  ...this.corsOptions,
1248
1269
  enabled: true,
@@ -1251,15 +1272,11 @@ class LiekoExpress {
1251
1272
 
1252
1273
  this._applyCors(req, res, finalCors);
1253
1274
  if (req.method === "OPTIONS") return;
1254
- }
1255
-
1256
- else if (this.corsOptions.enabled) {
1275
+ } else if (this.corsOptions.enabled) {
1257
1276
  this._applyCors(req, res, this.corsOptions);
1258
1277
  if (req.method === "OPTIONS") return;
1259
1278
  }
1260
-
1261
1279
  } else {
1262
-
1263
1280
  if (this.corsOptions.enabled) {
1264
1281
  this._applyCors(req, res, this.corsOptions);
1265
1282
  if (req.method === "OPTIONS") return;
@@ -1282,10 +1299,28 @@ class LiekoExpress {
1282
1299
  for (const mw of this.middlewares) {
1283
1300
  if (res.headersSent) return;
1284
1301
 
1285
- if (mw.path && !pathname.startsWith(mw.path)) continue;
1302
+ let shouldExecute = false;
1303
+ let pathToStrip = '';
1304
+
1305
+ if (mw.path === null) {
1306
+ shouldExecute = true;
1307
+ } else if (url.startsWith(mw.path)) {
1308
+ shouldExecute = true;
1309
+ pathToStrip = mw.path;
1310
+ }
1311
+
1312
+ if (!shouldExecute) continue;
1286
1313
 
1287
1314
  await new Promise((resolve, reject) => {
1315
+ const currentUrl = req.url;
1316
+
1317
+ if (pathToStrip) {
1318
+ req.url = url.substring(pathToStrip.length) || '/';
1319
+ }
1320
+
1288
1321
  const next = async (err) => {
1322
+ req.url = currentUrl;
1323
+
1289
1324
  if (err) {
1290
1325
  await this._runErrorHandlers(err, req, res);
1291
1326
  return resolve();
@@ -1342,25 +1377,31 @@ class LiekoExpress {
1342
1377
  }
1343
1378
 
1344
1379
  _enhanceRequest(req) {
1345
- const remoteIp = req.connection.remoteAddress || '';
1346
-
1347
- req.ips = [remoteIp];
1348
- let clientIp = remoteIp;
1380
+ req.app = this;
1381
+ let remoteIp = req.connection?.remoteAddress ||
1382
+ req.socket?.remoteAddress ||
1383
+ '';
1349
1384
 
1350
1385
  const forwardedFor = req.headers['x-forwarded-for'];
1386
+ let clientIp = remoteIp;
1387
+ let ipsChain = [remoteIp];
1351
1388
 
1352
1389
  if (forwardedFor) {
1353
- const proxyChain = forwardedFor.split(',').map(ip => ip.trim());
1354
-
1355
- if (this._isTrustedProxy(remoteIp)) {
1356
- clientIp = proxyChain[0];
1357
- req.ips = proxyChain;
1390
+ const chain = forwardedFor
1391
+ .split(',')
1392
+ .map(s => s.trim())
1393
+ .filter(Boolean);
1394
+
1395
+ if (chain.length > 0 && this._isTrustedProxy(remoteIp)) {
1396
+ clientIp = chain[0];
1397
+ ipsChain = chain;
1358
1398
  }
1359
1399
  }
1360
1400
 
1361
1401
  req.ip = this._parseIp(clientIp);
1362
-
1363
- req.protocol = req.headers['x-forwarded-proto'] || 'http';
1402
+ req.ips = ipsChain;
1403
+ req.ip.display = req.ip.ipv4 ?? '127.0.0.1';
1404
+ req.protocol = (req.headers['x-forwarded-proto'] || 'http').split(',')[0].trim();
1364
1405
  req.secure = req.protocol === 'https';
1365
1406
 
1366
1407
  const host = req.headers['host'];
@@ -1375,9 +1416,20 @@ class LiekoExpress {
1375
1416
 
1376
1417
  req.originalUrl = req.url;
1377
1418
  req.xhr = (req.headers['x-requested-with'] || '').toLowerCase() === 'xmlhttprequest';
1419
+
1420
+ req.get = (name) => {
1421
+ if (typeof name !== 'string') return undefined;
1422
+ const lower = name.toLowerCase();
1423
+ for (const key in req.headers) {
1424
+ if (key.toLowerCase() === lower) return req.headers[key];
1425
+ }
1426
+ return undefined;
1427
+ };
1428
+ req.header = req.get;
1378
1429
  }
1379
1430
 
1380
1431
  _enhanceResponse(req, res) {
1432
+ res.app = this;
1381
1433
  res.locals = {};
1382
1434
  let responseSent = false;
1383
1435
  let statusCode = 200;
@@ -1419,11 +1471,20 @@ class LiekoExpress {
1419
1471
  };
1420
1472
 
1421
1473
  const originalSetHeader = res.setHeader.bind(res);
1422
- res.setHeader = (name, value) => {
1474
+
1475
+ res.setHeader = function (name, value) {
1423
1476
  originalSetHeader(name, value);
1424
- return res;
1477
+ return this;
1478
+ };
1479
+
1480
+ res.set = function (name, value) {
1481
+ if (arguments.length === 1 && typeof name === 'object' && name !== null) {
1482
+ Object.entries(name).forEach(([k, v]) => originalSetHeader(k, v));
1483
+ } else {
1484
+ originalSetHeader(name, value);
1485
+ }
1486
+ return this;
1425
1487
  };
1426
- res.set = res.setHeader;
1427
1488
  res.header = res.setHeader;
1428
1489
 
1429
1490
  res.removeHeader = function (name) {
@@ -1436,16 +1497,111 @@ class LiekoExpress {
1436
1497
  return res;
1437
1498
  };
1438
1499
 
1500
+ res.render = async (view, options = {}, callback) => {
1501
+ if (responseSent) return res;
1502
+
1503
+ try {
1504
+ const locals = { ...res.locals, ...options };
1505
+ let viewPath = view;
1506
+ let ext = path.extname(view);
1507
+
1508
+ if (!ext) {
1509
+ ext = this.settings['view engine'];
1510
+ if (!ext) {
1511
+ ext = '.html';
1512
+ viewPath = view + ext;
1513
+ } else {
1514
+ if (!ext.startsWith('.')) ext = '.' + ext;
1515
+ viewPath = view + ext;
1516
+ }
1517
+ }
1518
+
1519
+ const viewsDir = this.settings.views || path.join(process.cwd(), 'views');
1520
+ let fullPath = path.join(viewsDir, viewPath);
1521
+ let fileExists = false;
1522
+ try {
1523
+ await fs.promises.access(fullPath);
1524
+ fileExists = true;
1525
+ } catch (err) {
1526
+ const extensions = ['.html', '.ejs', '.pug', '.hbs'];
1527
+ for (const tryExt of extensions) {
1528
+ if (tryExt === ext) continue;
1529
+ const tryPath = fullPath.replace(new RegExp(ext.replace('.', '\\.') + '$'), tryExt);
1530
+ try {
1531
+ await fs.promises.access(tryPath);
1532
+ fullPath = tryPath;
1533
+ ext = tryExt;
1534
+ fileExists = true;
1535
+ break;
1536
+ } catch (err2) { }
1537
+ }
1538
+ }
1539
+
1540
+ if (!fileExists) {
1541
+ const error = new Error(
1542
+ `View "${view}" not found in views directory "${viewsDir}".\n` +
1543
+ `Tried: ${fullPath}`
1544
+ );
1545
+ error.code = 'ENOENT';
1546
+ if (callback) return callback(error);
1547
+ throw error;
1548
+ }
1549
+
1550
+ const renderEngine = this.engines[ext];
1551
+
1552
+ if (!renderEngine) {
1553
+ throw new Error(
1554
+ `No engine registered for extension "${ext}".\n` +
1555
+ `Use app.engine("${ext}", renderFunction) to register one.`
1556
+ );
1557
+ }
1558
+
1559
+ return new Promise((resolve, reject) => {
1560
+ renderEngine(fullPath, locals, (err, html) => {
1561
+ if (err) {
1562
+ if (callback) {
1563
+ callback(err);
1564
+ resolve();
1565
+ } else {
1566
+ reject(err);
1567
+ }
1568
+ return;
1569
+ }
1570
+
1571
+ if (callback) {
1572
+ callback(null, html);
1573
+ resolve();
1574
+ } else {
1575
+ /*
1576
+ HBS cause error header already sent here ??
1577
+ */
1578
+ //res.statusCode = statusCode || 200;
1579
+ //res.setHeader('Content-Type', 'text/html; charset=utf-8');
1580
+ //responseSent = true;
1581
+ res.html(html);
1582
+ resolve();
1583
+ }
1584
+ });
1585
+ });
1586
+
1587
+ } catch (error) {
1588
+ if (callback) {
1589
+ callback(error);
1590
+ } else {
1591
+ throw error;
1592
+ }
1593
+ }
1594
+ };
1595
+
1439
1596
  res.json = (data) => {
1440
1597
  if (responseSent) return res;
1441
1598
 
1442
1599
  const json = JSON.stringify(data);
1443
1600
  const length = Buffer.byteLength(json);
1444
1601
 
1445
- res.writeHead(statusCode, buildHeaders('application/json; charset=utf-8', length));
1602
+ res.writeHead(statusCode || 200, buildHeaders('application/json; charset=utf-8', length));
1446
1603
 
1447
1604
  responseSent = true;
1448
- statusCode = 200;
1449
1605
  return res.end(json);
1450
1606
  };
1451
1607
 
@@ -1470,15 +1626,14 @@ class LiekoExpress {
1470
1626
 
1471
1627
  const length = Buffer.byteLength(body);
1472
1628
 
1473
- res.writeHead(statusCode, buildHeaders(contentType, length));
1629
+ res.writeHead(statusCode || 200, buildHeaders(contentType, length));
1474
1630
 
1475
1631
  responseSent = true;
1476
- statusCode = 200;
1477
1632
  return res.end(body);
1478
1633
  };
1479
1634
 
1480
- res.html = function (html, status = 200) {
1481
- res.statusCode = status;
1635
+ res.html = function (html, status) {
1636
+ res.statusCode = status !== undefined ? status : (statusCode || 200);
1482
1637
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1483
1638
  res.end(html);
1484
1639
  };
@@ -1573,6 +1728,42 @@ class LiekoExpress {
1573
1728
  };
1574
1729
  }
1575
1730
 
1731
+ _defaultHtmlEngine(filePath, locals, callback) {
1732
+ fs.readFile(filePath, 'utf-8', (err, content) => {
1733
+ if (err) return callback(err);
1734
+
1735
+ let rendered = content;
1736
+
1737
+ Object.keys(locals).forEach(key => {
1738
+ if (locals[key] !== undefined && locals[key] !== null) {
1739
+ const safeRegex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
1740
+ const unsafeRegex = new RegExp(`{{{\\s*${key}\\s*}}}`, 'g');
1741
+
1742
+ if (safeRegex.test(rendered)) {
1743
+ const escaped = this._escapeHtml(String(locals[key]));
1744
+ rendered = rendered.replace(safeRegex, escaped);
1745
+ }
1746
+
1747
+ if (unsafeRegex.test(rendered)) {
1748
+ rendered = rendered.replace(unsafeRegex, String(locals[key]));
1749
+ }
1750
+ }
1751
+ });
1752
+
1753
+ callback(null, rendered);
1754
+ });
1755
+ }
1756
+
1757
+ _escapeHtml(text) {
1758
+ if (typeof text !== 'string') return text;
1759
+ return text
1760
+ .replace(/&/g, '&amp;')
1761
+ .replace(/</g, '&lt;')
1762
+ .replace(/>/g, '&gt;')
1763
+ .replace(/"/g, '&quot;')
1764
+ .replace(/'/g, '&#039;');
1765
+ }
1766
+
1576
1767
  async _runMiddleware(handler, req, res) {
1577
1768
  return new Promise((resolve, reject) => {
1578
1769
  const next = (err) => err ? reject(err) : resolve();
@@ -1620,123 +1811,105 @@ class LiekoExpress {
1620
1811
  bodySizeFormatted = `${(bodySize / (1024 * 1024 * 1024)).toFixed(2)} GB`;
1621
1812
  }
1622
1813
 
1623
- console.log(
1624
- `\n🟦 DEBUG REQUEST` +
1625
- `\n→ ${req.method} ${req.originalUrl}` +
1626
- `\n→ IP: ${req.ip.ipv4}` +
1627
- `\n→ Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m` +
1628
- `\n→ Duration: ${timeFormatted}` +
1629
- `\n→ Body Size: ${bodySizeFormatted}` +
1630
- `\n→ Params: ${JSON.stringify(req.params || {})}` +
1631
- `\n→ Query: ${JSON.stringify(req.query || {})}` +
1632
- `\n→ Body: ${JSON.stringify(req.body || {}).substring(0, 200)}${JSON.stringify(req.body || {}).length > 200 ? '...' : ''}` +
1633
- `\n→ Files: ${Object.keys(req.files || {}).join(', ')}` +
1634
- `\n---------------------------------------------\n`
1635
- );
1636
- }
1637
-
1638
- _logCorsDebug(req, opts) {
1639
- if (!opts.debug) return;
1814
+ const logLines = [
1815
+ '[DEBUG REQUEST]',
1816
+ `→ ${req.method} ${req.originalUrl}`,
1817
+ `→ IP: ${req.ip.ipv4 || '127.0.0.1'}`,
1818
+ `→ Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m`,
1819
+ `→ Duration: ${timeFormatted}`,
1820
+ ];
1640
1821
 
1641
- console.log("\n[CORS DEBUG]");
1642
- console.log("Request:", req.method, req.url);
1643
- console.log("Origin:", req.headers.origin || "none");
1644
-
1645
- console.log("Applied CORS Policy:");
1646
- console.log(" - Access-Control-Allow-Origin:", opts.origin);
1647
- console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
1648
- console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
1649
-
1650
- if (opts.credentials) {
1651
- console.log(" - Access-Control-Allow-Credentials: true");
1822
+ if (req.params && Object.keys(req.params).length > 0) {
1823
+ logLines.push(`→ Params: ${JSON.stringify(req.params)}`);
1652
1824
  }
1653
1825
 
1654
- if (opts.exposedHeaders?.length) {
1655
- console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
1826
+ if (req.query && Object.keys(req.query).length > 0) {
1827
+ logLines.push(`→ Query: ${JSON.stringify(req.query)}`);
1656
1828
  }
1657
1829
 
1658
- console.log(" - Max-Age:", opts.maxAge);
1659
-
1660
- if (req.method === "OPTIONS") {
1661
- console.log("Preflight request handled with status 204\n");
1830
+ if (req.body && Object.keys(req.body).length > 0) {
1831
+ const bodyStr = JSON.stringify(req.body);
1832
+ const truncated = bodyStr.substring(0, 200) + (bodyStr.length > 200 ? '...' : '');
1833
+ logLines.push(`→ Body: ${truncated}`);
1834
+ logLines.push(`→ Body Size: ${bodySizeFormatted}`);
1662
1835
  }
1663
- }
1664
-
1665
- _buildRouteTree() {
1666
- const tree = {};
1667
-
1668
- for (const route of this.routes) {
1669
- let node = tree;
1670
-
1671
- for (const group of route.groupChain) {
1672
- const key = group.basePath;
1673
-
1674
- if (!node[key]) {
1675
- node[key] = {
1676
- __meta: group,
1677
- __children: {},
1678
- __routes: []
1679
- };
1680
- }
1681
1836
 
1682
- node = node[key].__children;
1683
- }
1684
-
1685
- if (!node.__routes) node.__routes = [];
1686
- node.__routes.push(route);
1837
+ if (req.files && Object.keys(req.files).length > 0) {
1838
+ logLines.push(`→ Files: ${Object.keys(req.files).join(', ')}`);
1687
1839
  }
1688
1840
 
1689
- return tree;
1841
+ logLines.push('---------------------------------------------');
1842
+ console.log('\n' + logLines.join('\n') + '\n');
1690
1843
  }
1691
-
1844
+
1692
1845
  listRoutes() {
1693
- return this.routes.map(route => ({
1694
- method: route.method,
1695
- path: route.path,
1696
- middlewares: route.middlewares.length
1697
- }));
1698
- }
1699
-
1700
- printRoutes() {
1701
- console.log('\n📌 Registered Routes:\n');
1846
+ const routeEntries = [];
1702
1847
 
1703
- this.routes.forEach(r => {
1704
- console.log(
1705
- `${r.method.padEnd(6)} ${r.path} ` +
1706
- `(mw: ${r.middlewares.length})`
1848
+ this.routes.forEach(route => {
1849
+ const existing = routeEntries.find(
1850
+ entry => entry.method === route.method &&
1851
+ entry.handler === route.handler
1707
1852
  );
1853
+
1854
+ if (existing) {
1855
+ if (!Array.isArray(existing.path)) {
1856
+ existing.path = [existing.path];
1857
+ }
1858
+ existing.path.push(route.path);
1859
+ } else {
1860
+ routeEntries.push({
1861
+ method: route.method,
1862
+ path: route.path,
1863
+ middlewares: route.middlewares.length,
1864
+ handler: route.handler
1865
+ });
1866
+ }
1708
1867
  });
1709
1868
 
1710
- console.log('');
1869
+ return routeEntries.map(entry => ({
1870
+ method: entry.method,
1871
+ path: entry.path,
1872
+ middlewares: entry.middlewares
1873
+ }));
1711
1874
  }
1712
1875
 
1713
- printRoutesNested(tree = null, indent = '') {
1714
- if (!tree) {
1715
- console.log('\n📌 Nested Routes:\n');
1716
- tree = this._buildRouteTree();
1876
+ printRoutes() {
1877
+ if (this.routes.length === 0) {
1878
+ console.log('\nNo routes registered.\n');
1879
+ return;
1717
1880
  }
1718
1881
 
1719
- const prefix = indent + ' ';
1720
-
1721
- for (const [path, data] of Object.entries(tree)) {
1722
- if (path.startsWith("__")) continue;
1882
+ console.log(`\nRegistered Routes: ${this.routes.length}\n`);
1723
1883
 
1724
- const mwCount = data.__meta.middlewares.length;
1725
- console.log(`${indent}${path} [${mwCount} mw]`);
1884
+ const grouped = new Map();
1726
1885
 
1727
- if (data.__routes && data.__routes.length) {
1728
- for (const route of data.__routes) {
1729
- const shortPath = route.path.replace(path, '') || '/';
1730
- console.log(`${prefix}${route.method.padEnd(6)} ${shortPath}`);
1731
- }
1886
+ for (const route of this.routes) {
1887
+ const key = `${route.method}|${route.handler}`;
1888
+ if (!grouped.has(key)) {
1889
+ grouped.set(key, {
1890
+ method: route.method,
1891
+ paths: [],
1892
+ mw: route.middlewares.length
1893
+ });
1894
+ }
1895
+ const entry = grouped.get(key);
1896
+ const p = route.path || '/';
1897
+ if (!entry.paths.includes(p)) {
1898
+ entry.paths.push(p);
1732
1899
  }
1733
- this.printRoutesNested(data.__children, prefix);
1734
1900
  }
1735
1901
 
1736
- if (tree.__routes) {
1737
- for (const route of tree.__routes) {
1738
- console.log(`${indent}${route.method.padEnd(6)} ${route.path}`);
1739
- }
1902
+ const sorted = Array.from(grouped.values()).sort((a, b) => {
1903
+ if (a.method !== b.method) return a.method.localeCompare(b.method);
1904
+ return a.paths[0].localeCompare(b.paths[0]);
1905
+ });
1906
+
1907
+ for (const r of sorted) {
1908
+ const pathStr = r.paths.length === 1
1909
+ ? r.paths[0]
1910
+ : r.paths.join(', ');
1911
+
1912
+ console.log(` \x1b[36m${r.method.padEnd(7)}\x1b[0m \x1b[33m${pathStr}\x1b[0m \x1b[90m(mw: ${r.mw})\x1b[0m`);
1740
1913
  }
1741
1914
  }
1742
1915
 
@@ -1760,8 +1933,9 @@ function Router() {
1760
1933
 
1761
1934
  module.exports = Lieko;
1762
1935
  module.exports.Router = Router;
1936
+
1763
1937
  module.exports.Schema = Schema;
1764
- module.exports.schema = (...args) => new Schema(...args);
1938
+ module.exports.createSchema = (...args) => new Schema(...args);
1765
1939
  module.exports.validators = validators;
1766
1940
  module.exports.validate = validate;
1767
1941
  module.exports.validatePartial = validatePartial;