lieko-express 0.0.6 → 0.0.7

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # **Lieko-express — A Modern, Minimal, REST API Framework for Node.js**
1
+ # **Lieko-express — A Modern, Minimal, express-like Framework for Node.js**
2
2
 
3
- A lightweight, fast, and modern Node.js REST API framework built on top of the native `http` module. Zero external dependencies for core functionality.
3
+ A lightweight, fast, and modern Node.js framework built on top of the native `http` module. Zero external dependencies for core functionality.
4
4
 
5
5
  ![Performance](https://img.shields.io/badge/Performance-49%25_faster_than_Express-00d26a?style=for-the-badge)
6
6
 
@@ -388,7 +388,7 @@ app.use(async (req, res, next) => {
388
388
  Lieko-Express includes a **fully built-in, high-performance CORS engine**, with:
389
389
 
390
390
  * Global CORS (`app.cors()`)
391
- * Route-level CORS (`app.get("/test", { cors: {...} }, handler)`—coming soon if you want it)
391
+ * Route-level CORS (`app.get("/test", { cors: {...} }, handler)`)
392
392
  * Wildcard origins (`https://*.example.com`)
393
393
  * Multiple allowed origins
394
394
  * Strict mode (reject unknown origins)
@@ -907,8 +907,6 @@ const app = Lieko(); // Ready to handle JSON, form-data, files, etc.
907
907
  | `application/x-www-form-urlencoded` | **1mb** | ~1,048,576 bytes |
908
908
  | `multipart/form-data` (file uploads) | **10mb** | ~10,485,760 bytes |
909
909
 
910
- That’s already **10× more generous** than Express’s default 100kb!
911
-
912
910
  ### Change Limits — Three Super-Simple Ways
913
911
 
914
912
  #### 1. Per content-type (most common)
@@ -35,6 +35,8 @@ declare namespace Lieko {
35
35
 
36
36
  // headers helpers
37
37
  setHeader(name: string, value: string | number): void;
38
+ set(name: string | Record<string, string | number>, value?: string | number): void;
39
+ header(name: string, value: string | number): void;
38
40
  getHeader(name: string): string | number | string[] | undefined;
39
41
  removeHeader(name: string): void;
40
42
  headersSent?: boolean;
@@ -69,20 +71,22 @@ declare namespace Lieko {
69
71
  interface Response extends ResponseBase {
70
72
  // Rich helpers provided by Lieko
71
73
  ok(data?: any, message?: string): void;
74
+ success: (data?: any, message?: string) => void;
72
75
  created(data?: any, message?: string): void;
73
- accepted(data?: any, message?: string): void;
74
76
  noContent(): void;
77
+ accepted(data?: any, message?: string): void;
75
78
 
76
79
  badRequest(message?: string, details?: any): void;
77
80
  unauthorized(message?: string, details?: any): void;
78
81
  forbidden(message?: string, details?: any): void;
79
82
  notFound(message?: string, details?: any): void;
80
83
  error(message?: string, status?: number, details?: any): void;
84
+ fail: (message?: string, status?: number, details?: any) => void;
85
+ serverError(message?: string, details?: any): void;
81
86
 
82
87
  // convenience helpers sometimes provided
83
- paginated?(items: any[], meta: any): void;
84
- file?(pathOrBuffer: string | Buffer, filename?: string): void;
85
- download?(path: string, filename?: string): void;
88
+ paginated?(items: any[], total: number, message?: string): void;
89
+ html?(html: string, status?: number): void;
86
90
 
87
91
  // short alias
88
92
  statusCode?: number;
@@ -132,35 +136,57 @@ declare namespace Lieko {
132
136
 
133
137
  // -------------- Validation / Schema (loose typing to match runtime) --------------
134
138
  // The framework has a validation system — keep it flexible (user can extend)
135
- type ValidatorFn = (value: any) => boolean | string | Promise<boolean | string>;
136
-
137
- interface SchemaField {
138
- type?: string | string[]; // "string", "number", etc.
139
- required?: boolean;
140
- validators?: ValidatorFn[];
141
- default?: any;
142
- // additional user metadata allowed
143
- [key: string]: any;
139
+ type ValidatorFn = (value: any, field: string, data: any) => { field: string; message: string; type: string } | null;
140
+
141
+ interface Validators {
142
+ required(message?: string): ValidatorFn;
143
+ requiredTrue(message?: string): ValidatorFn;
144
+ optional(): ValidatorFn;
145
+ string(message?: string): ValidatorFn;
146
+ number(message?: string): ValidatorFn;
147
+ boolean(message?: string): ValidatorFn;
148
+ integer(message?: string): ValidatorFn;
149
+ positive(message?: string): ValidatorFn;
150
+ negative(message?: string): ValidatorFn;
151
+ email(message?: string): ValidatorFn;
152
+ min(minValue: number, message?: string): ValidatorFn;
153
+ max(maxValue: number, message?: string): ValidatorFn;
154
+ length(n: number, message?: string): ValidatorFn;
155
+ minLength(minLength: number, message?: string): ValidatorFn;
156
+ maxLength(maxLength: number, message?: string): ValidatorFn;
157
+ pattern(regex: RegExp, message?: string): ValidatorFn;
158
+ oneOf(allowedValues: any[], message?: string): ValidatorFn;
159
+ notOneOf(values: any[], message?: string): ValidatorFn;
160
+ custom(validatorFn: (value: any, data: any) => boolean, message?: string): ValidatorFn;
161
+ equal(expectedValue: any, message?: string): ValidatorFn;
162
+ mustBeTrue(message?: string): ValidatorFn;
163
+ mustBeFalse(message?: string): ValidatorFn;
164
+ date(message?: string): ValidatorFn;
165
+ before(limit: string | Date, message?: string): ValidatorFn;
166
+ after(limit: string | Date, message?: string): ValidatorFn;
167
+ startsWith(prefix: string, message?: string): ValidatorFn;
168
+ endsWith(suffix: string, message?: string): ValidatorFn;
144
169
  }
145
170
 
146
- interface SchemaDefinition {
147
- [field: string]: SchemaField | string; // string shorthand for type
171
+ class Schema {
172
+ constructor(rules: Record<string, ValidatorFn[]>);
173
+ rules: Record<string, ValidatorFn[]>;
174
+ fields: Record<string, ValidatorFn[]>;
175
+ validate(data: Record<string, any>): true | never;
148
176
  }
149
177
 
150
- interface Schema {
151
- definition: SchemaDefinition;
152
- validate(obj: any, options?: { partial?: boolean }): { valid: boolean; errors?: any };
153
- }
178
+ function validate(schema: Schema): Handler;
179
+
180
+ function validatePartial(schema: Schema): Handler;
154
181
 
155
182
  // -------------- Routes / Options --------------
156
- interface RouteOptions {
157
- cors?: Partial<CorsOptions>;
158
- bodyParserOptions?: Partial<BodyParserOptions>;
159
- middlewares?: Handler[];
160
- // validation
161
- schema?: Schema | SchemaDefinition;
162
- // other custom per-route options
163
- [key: string]: any;
183
+ interface Route {
184
+ method: string;
185
+ path: string;
186
+ handler: Handler;
187
+ middlewares: Handler[];
188
+ pattern: RegExp;
189
+ groupChain: any[];
164
190
  }
165
191
 
166
192
  // -------------- App interface --------------
@@ -168,77 +194,71 @@ declare namespace Lieko {
168
194
  // route methods (typed path param extraction)
169
195
  get<Path extends string, Q = any, B = any>(
170
196
  path: Path,
171
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
172
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
197
+ ...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
173
198
  ): this;
174
199
 
175
200
  post<Path extends string, Q = any, B = any>(
176
201
  path: Path,
177
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
178
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
202
+ ...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
179
203
  ): this;
180
204
 
181
205
  put<Path extends string, Q = any, B = any>(
182
206
  path: Path,
183
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
184
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
207
+ ...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
185
208
  ): this;
186
209
 
187
210
  patch<Path extends string, Q = any, B = any>(
188
211
  path: Path,
189
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
190
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
212
+ ...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
191
213
  ): this;
192
214
 
193
215
  delete<Path extends string, Q = any, B = any>(
194
216
  path: Path,
195
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
196
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
217
+ ...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
197
218
  ): this;
198
219
 
199
- options<Path extends string, Q = any, B = any>(
220
+ all<Path extends string, Q = any, B = any>(
200
221
  path: Path,
201
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
202
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
222
+ ...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
203
223
  ): this;
204
224
 
205
- head<Path extends string, Q = any, B = any>(
206
- path: Path,
207
- optionsOrHandler?: RouteOptions | Handler<ExtractRouteParams<Path>, Q, B>,
208
- maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
209
- ): this;
210
-
211
- // manual route API
212
- route(method: string, path: string, options: RouteOptions, handler: Handler): this;
213
-
214
225
  // middleware
215
- use(handler: Handler): this;
216
- use(path: string, handler: Handler): this;
226
+ use(handler: Handler | App): this;
227
+ use(path: string, handler: Handler | App): this;
217
228
 
218
229
  // grouping
219
- group(prefix: string, cb: (router: App) => void): this;
230
+ group(basePath: string, callback: (group: App) => void): this;
220
231
 
221
232
  // CORS
222
- cors(options?: Partial<CorsOptions>): this;
233
+ cors(options?: Partial<CorsOptions>): Handler;
223
234
 
224
- // body parser options at app-level
225
- bodyParser(options: Partial<BodyParserOptions>): this;
235
+ // body parser
236
+ bodyParser: {
237
+ json(options?: JsonBodyOptions): Handler;
238
+ urlencoded(options?: UrlencodedOptions): Handler;
239
+ multipart(options?: MultipartOptions): Handler;
240
+ };
226
241
 
227
- // validation / schema helpers
228
- schema(name: string, definition: SchemaDefinition | Schema): Schema;
229
- validate(schemaOrDef: string | Schema | SchemaDefinition): Handler;
242
+ // static files
243
+ static(root: string, options?: { maxAge?: number; index?: string }): Handler;
230
244
 
231
- // error / notFound handlers
232
- notFound(handler: Handler): this;
233
- error(handler: (err: any, req: Request, res: Response) => any): this;
245
+ // error handler
246
+ error(res: Response, obj: any): void;
247
+
248
+ // settings
249
+ set(key: string, value: any): this;
250
+ get(key: string): any;
251
+
252
+ // debug
253
+ debug(value?: boolean | string): this;
234
254
 
235
255
  // utilities
256
+ listRoutes(): { method: string; path: string | string[]; middlewares: number }[];
236
257
  printRoutes(): void;
237
- close(): Promise<void> | void;
238
258
 
239
259
  // server control
240
- listen(port: number, callback?: () => void): any;
241
- listen(port: number, host: string, callback?: () => void): any;
260
+ listen(port: number, host?: string, callback?: () => void): any;
261
+ listen(...args: any[]): any;
242
262
  }
243
263
 
244
264
  // -------------- Factory / Constructor --------------
@@ -247,6 +267,9 @@ declare namespace Lieko {
247
267
  cors?: Partial<CorsOptions>;
248
268
  bodyParser?: Partial<BodyParserOptions>;
249
269
  trustProxy?: boolean | string | string[];
270
+ debug?: boolean;
271
+ allowTrailingSlash?: boolean;
272
+ strictTrailingSlash?: boolean;
250
273
  // other global options
251
274
  [key: string]: any;
252
275
  }
@@ -255,10 +278,16 @@ declare namespace Lieko {
255
278
  (opts?: ConstructorOptions): App;
256
279
 
257
280
  // expose helpers statically if present in runtime
258
- createApp(opts?: ConstructorOptions): App;
281
+ Router: () => App;
282
+ Schema: typeof Schema;
283
+ schema: (...args: any[]) => Schema;
284
+ validators: Validators;
285
+ validate: typeof validate;
286
+ validatePartial: (schema: Schema) => Handler;
287
+ ValidationError: typeof ValidationError;
288
+ static: (root: string, options?: { maxAge?: number; index?: string }) => Handler;
259
289
  }
260
290
  }
261
291
 
262
- // Export as CommonJS-compatible factory function
263
292
  declare const Lieko: Lieko.LiekoStatic;
264
- export = Lieko;
293
+ export = Lieko;
package/lieko-express.js CHANGED
@@ -1,5 +1,7 @@
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
6
  process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
5
7
 
@@ -420,9 +422,13 @@ class LiekoExpress {
420
422
  'x-powered-by': 'lieko-express',
421
423
  'trust proxy': false,
422
424
  strictTrailingSlash: true,
423
- allowTrailingSlash: false,
425
+ allowTrailingSlash: true,
426
+ views: path.join(process.cwd(), "views"),
427
+ "view engine": null
424
428
  };
425
429
 
430
+ this.engines = {};
431
+
426
432
  this.bodyParserOptions = {
427
433
  json: {
428
434
  limit: '10mb',
@@ -545,13 +551,23 @@ class LiekoExpress {
545
551
  }
546
552
  }
547
553
 
554
+ debug(value = true) {
555
+ if (typeof value === 'string') {
556
+ value = value.toLowerCase() === 'true';
557
+ }
558
+ this.set('debug', value);
559
+ return this;
560
+ }
561
+
548
562
  set(name, value) {
549
563
  this.settings[name] = value;
550
564
  return this;
551
565
  }
552
566
 
553
- get(name) {
554
- return this.settings[name];
567
+ engine(ext, renderFunction) {
568
+ if (!ext.startsWith(".")) ext = "." + ext;
569
+ this.engines[ext] = renderFunction;
570
+ return this;
555
571
  }
556
572
 
557
573
  enable(name) {
@@ -576,6 +592,7 @@ class LiekoExpress {
576
592
  if (options.limit) {
577
593
  this.bodyParserOptions.json.limit = options.limit;
578
594
  this.bodyParserOptions.urlencoded.limit = options.limit;
595
+ this.bodyParserOptions.multipart.limit = options.limit;
579
596
  }
580
597
  if (options.extended !== undefined) {
581
598
  this.bodyParserOptions.urlencoded.extended = options.extended;
@@ -617,7 +634,7 @@ class LiekoExpress {
617
634
  if (typeof limit === 'number') return limit;
618
635
 
619
636
  const match = limit.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
620
- if (!match) return 1048576; // 1mb par défaut
637
+ if (!match) return 1048576;
621
638
 
622
639
  const value = parseFloat(match[1]);
623
640
  const unit = (match[2] || 'b').toLowerCase();
@@ -810,9 +827,13 @@ class LiekoExpress {
810
827
  });
811
828
  }
812
829
 
813
- get(path, ...handlers) {
814
- this._addRoute('GET', path, ...handlers);
815
- return this;
830
+ get(...args) {
831
+ if (args.length === 1 && typeof args[0] === 'string' && !args[0].startsWith('/')) {
832
+ return this.settings[args[0]];
833
+ } else {
834
+ this._addRoute('GET', ...args);
835
+ return this;
836
+ }
816
837
  }
817
838
 
818
839
  post(path, ...handlers) {
@@ -867,6 +888,12 @@ class LiekoExpress {
867
888
  all(path, ...handlers) { return this._call('all', path, handlers); },
868
889
 
869
890
  use(pathOrMw, ...rest) {
891
+ if (typeof pathOrMw === 'object' && pathOrMw instanceof LiekoExpress) {
892
+ const finalPath = fullBase === '/' ? '/' : fullBase;
893
+ parent.use(finalPath, ...middlewares, pathOrMw);
894
+ return subApp;
895
+ }
896
+
870
897
  if (typeof pathOrMw === "function") {
871
898
  parent.use(fullBase, ...middlewares, pathOrMw);
872
899
  return subApp;
@@ -904,16 +931,66 @@ class LiekoExpress {
904
931
  if (isAsync) return;
905
932
 
906
933
  if (handler.length < 3) {
907
- console.warn(`
908
- ⚠️ WARNING: Middleware executed without a 'next' parameter.
909
- This middleware may block the request pipeline.
934
+ const funcString = handler.toString();
935
+ const stack = new Error().stack;
936
+ let userFileInfo = 'unknown location';
937
+ let userLine = '';
938
+
939
+ if (stack) {
940
+ const lines = stack.split('\n');
941
+
942
+ for (let i = 0; i < lines.length; i++) {
943
+ const line = lines[i].trim();
944
+
945
+ if (line.includes('lieko-express.js') ||
946
+ line.includes('_checkMiddleware') ||
947
+ line.includes('at LiekoExpress.') ||
948
+ line.includes('at Object.<anonymous>') ||
949
+ line.includes('at Module._compile')) {
950
+ continue;
951
+ }
952
+
953
+ const fileMatch = line.match(/\(?(.+?):(\d+):(\d+)\)?$/);
954
+ if (fileMatch) {
955
+ const filePath = fileMatch[1];
956
+ const lineNumber = fileMatch[2];
957
+
958
+ const shortPath = filePath.replace(process.cwd(), '.');
959
+ userFileInfo = `${shortPath}:${lineNumber}`;
960
+ userLine = line;
961
+ break;
962
+ }
963
+ }
964
+ }
910
965
 
911
- Offending middleware:
912
- ${handler.toString().split('\n')[0].substring(0, 120)}...
966
+ const firstLine = funcString.split('\n')[0];
967
+ const secondLine = funcString.split('\n')[1] || '';
968
+ const thirdLine = funcString.split('\n')[2] || '';
913
969
 
914
- Fix: Add 'next' as third parameter and call it:
915
- (req, res, next) => { /* your code */ next(); }
916
- `);
970
+ const yellow = '\x1b[33m';
971
+ const red = '\x1b[31m';
972
+ const cyan = '\x1b[36m';
973
+ const reset = '\x1b[0m';
974
+ const bold = '\x1b[1m';
975
+
976
+ console.warn(`
977
+ ${yellow}${bold}⚠️ WARNING: Middleware missing 'next' parameter${reset}
978
+ ${yellow}This middleware may block the request pipeline.${reset}
979
+
980
+ ${cyan}📍 Defined at:${reset} ${userFileInfo}
981
+ ${userLine ? `${cyan} Stack trace:${reset} ${userLine}` : ''}
982
+
983
+ ${cyan}🔧 Middleware definition:${reset}
984
+ ${yellow}${firstLine.substring(0, 100)}${firstLine.length > 100 ? '...' : ''}${reset}
985
+ ${secondLine ? `${yellow} ${secondLine.substring(0, 100)}${secondLine.length > 100 ? '...' : ''}${reset}` : ''}
986
+ ${thirdLine ? `${yellow} ${thirdLine.substring(0, 100)}${thirdLine.length > 100 ? '...' : ''}${reset}` : ''}
987
+
988
+ ${red}${bold}FIX:${reset} Add 'next' as third parameter and call it:
989
+ ${cyan} (req, res, next) => {
990
+ // your code here
991
+ next(); // ← Don't forget to call next()
992
+ }${reset}
993
+ `);
917
994
  }
918
995
  }
919
996
 
@@ -997,12 +1074,257 @@ class LiekoExpress {
997
1074
  throw new Error('Invalid use() arguments');
998
1075
  }
999
1076
 
1077
+ static(root, options = {}) {
1078
+ const opts = {
1079
+ maxAge: options.maxAge || 0,
1080
+ index: options.index !== undefined ? options.index : 'index.html',
1081
+ dotfiles: options.dotfiles || 'ignore',
1082
+ etag: options.etag !== undefined ? options.etag : true,
1083
+ extensions: options.extensions || false,
1084
+ fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
1085
+ immutable: options.immutable || false,
1086
+ lastModified: options.lastModified !== undefined ? options.lastModified : true,
1087
+ redirect: options.redirect !== undefined ? options.redirect : true,
1088
+ setHeaders: options.setHeaders || null,
1089
+ cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
1090
+ };
1091
+
1092
+ const mimeTypes = {
1093
+ '.html': 'text/html; charset=utf-8',
1094
+ '.htm': 'text/html; charset=utf-8',
1095
+ '.css': 'text/css; charset=utf-8',
1096
+ '.js': 'application/javascript; charset=utf-8',
1097
+ '.mjs': 'application/javascript; charset=utf-8',
1098
+ '.json': 'application/json; charset=utf-8',
1099
+ '.xml': 'application/xml; charset=utf-8',
1100
+ '.txt': 'text/plain; charset=utf-8',
1101
+ '.md': 'text/markdown; charset=utf-8',
1102
+ '.jpg': 'image/jpeg',
1103
+ '.jpeg': 'image/jpeg',
1104
+ '.png': 'image/png',
1105
+ '.gif': 'image/gif',
1106
+ '.svg': 'image/svg+xml',
1107
+ '.webp': 'image/webp',
1108
+ '.ico': 'image/x-icon',
1109
+ '.bmp': 'image/bmp',
1110
+ '.tiff': 'image/tiff',
1111
+ '.tif': 'image/tiff',
1112
+ '.mp3': 'audio/mpeg',
1113
+ '.wav': 'audio/wav',
1114
+ '.ogg': 'audio/ogg',
1115
+ '.m4a': 'audio/mp4',
1116
+ '.aac': 'audio/aac',
1117
+ '.flac': 'audio/flac',
1118
+ '.mp4': 'video/mp4',
1119
+ '.webm': 'video/webm',
1120
+ '.ogv': 'video/ogg',
1121
+ '.avi': 'video/x-msvideo',
1122
+ '.mov': 'video/quicktime',
1123
+ '.wmv': 'video/x-ms-wmv',
1124
+ '.flv': 'video/x-flv',
1125
+ '.mkv': 'video/x-matroska',
1126
+ '.woff': 'font/woff',
1127
+ '.woff2': 'font/woff2',
1128
+ '.ttf': 'font/ttf',
1129
+ '.otf': 'font/otf',
1130
+ '.eot': 'application/vnd.ms-fontobject',
1131
+ '.zip': 'application/zip',
1132
+ '.rar': 'application/x-rar-compressed',
1133
+ '.tar': 'application/x-tar',
1134
+ '.gz': 'application/gzip',
1135
+ '.7z': 'application/x-7z-compressed',
1136
+ '.pdf': 'application/pdf',
1137
+ '.doc': 'application/msword',
1138
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1139
+ '.xls': 'application/vnd.ms-excel',
1140
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1141
+ '.ppt': 'application/vnd.ms-powerpoint',
1142
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1143
+ '.wasm': 'application/wasm',
1144
+ '.csv': 'text/csv; charset=utf-8'
1145
+ };
1146
+
1147
+ const getMimeType = (filePath) => {
1148
+ const ext = path.extname(filePath).toLowerCase();
1149
+ return mimeTypes[ext] || 'application/octet-stream';
1150
+ };
1151
+
1152
+ const generateETag = (stats) => {
1153
+ const mtime = stats.mtime.getTime().toString(16);
1154
+ const size = stats.size.toString(16);
1155
+ return `W/"${size}-${mtime}"`;
1156
+ };
1157
+
1158
+ return async (req, res, next) => {
1159
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1160
+ return next();
1161
+ }
1162
+
1163
+ try {
1164
+ let pathname = req.url;
1165
+ const qIndex = pathname.indexOf('?');
1166
+ if (qIndex !== -1) {
1167
+ pathname = pathname.substring(0, qIndex);
1168
+ }
1169
+
1170
+ if (pathname === '') {
1171
+ pathname = '/';
1172
+ }
1173
+
1174
+ try {
1175
+ pathname = decodeURIComponent(pathname);
1176
+ } catch (e) {
1177
+ if (opts.fallthrough) return next();
1178
+ return res.status(400).send('Bad Request');
1179
+ }
1180
+
1181
+ let filePath = pathname === '/' ? root : path.join(root, pathname);
1182
+
1183
+ const resolvedPath = path.resolve(filePath);
1184
+ const resolvedRoot = path.resolve(root);
1185
+
1186
+ if (!resolvedPath.startsWith(resolvedRoot)) {
1187
+ if (opts.fallthrough) return next();
1188
+ return res.status(403).send('Forbidden');
1189
+ }
1190
+
1191
+ let stats;
1192
+ try {
1193
+ stats = await fs.promises.stat(filePath);
1194
+ } catch (err) {
1195
+ if (pathname === '/' && opts.index) {
1196
+ const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
1197
+ for (const indexFile of indexes) {
1198
+ const indexPath = path.join(root, indexFile);
1199
+ try {
1200
+ stats = await fs.promises.stat(indexPath);
1201
+ if (stats.isFile()) {
1202
+ filePath = indexPath;
1203
+ break;
1204
+ }
1205
+ } catch (e) {}
1206
+ }
1207
+ }
1208
+
1209
+ if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
1210
+ let found = false;
1211
+ for (const ext of opts.extensions) {
1212
+ const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
1213
+ try {
1214
+ stats = await fs.promises.stat(testPath);
1215
+ filePath = testPath;
1216
+ found = true;
1217
+ break;
1218
+ } catch (e) {}
1219
+ }
1220
+ if (!found) return next();
1221
+ } else if (!stats) {
1222
+ return next();
1223
+ }
1224
+ }
1225
+
1226
+ if (stats.isDirectory()) {
1227
+ if (opts.redirect && !pathname.endsWith('/')) {
1228
+ const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
1229
+ const redirectUrl = pathname + '/' + query;
1230
+ return res.redirect(redirectUrl, 301);
1231
+ }
1232
+
1233
+ if (opts.index) {
1234
+ const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
1235
+
1236
+ for (const indexFile of indexes) {
1237
+ const indexPath = path.join(filePath, indexFile);
1238
+ try {
1239
+ const indexStats = await fs.promises.stat(indexPath);
1240
+ if (indexStats.isFile()) {
1241
+ filePath = indexPath;
1242
+ stats = indexStats;
1243
+ break;
1244
+ }
1245
+ } catch (e) {}
1246
+ }
1247
+
1248
+ if (stats.isDirectory()) {
1249
+ if (opts.fallthrough) return next();
1250
+ return res.status(404).send('Not Found');
1251
+ }
1252
+ } else {
1253
+ if (opts.fallthrough) return next();
1254
+ return res.status(404).send('Not Found');
1255
+ }
1256
+ }
1257
+
1258
+ if (opts.etag) {
1259
+ const etag = generateETag(stats);
1260
+ const ifNoneMatch = req.headers['if-none-match'];
1261
+
1262
+ if (ifNoneMatch === etag) {
1263
+ res.statusCode = 304;
1264
+ res.end();
1265
+ return;
1266
+ }
1267
+
1268
+ res.setHeader('ETag', etag);
1269
+ }
1270
+
1271
+ if (opts.lastModified) {
1272
+ const lastModified = stats.mtime.toUTCString();
1273
+ const ifModifiedSince = req.headers['if-modified-since'];
1274
+
1275
+ if (ifModifiedSince === lastModified) {
1276
+ res.statusCode = 304;
1277
+ res.end();
1278
+ return;
1279
+ }
1280
+
1281
+ res.setHeader('Last-Modified', lastModified);
1282
+ }
1283
+
1284
+ if (opts.cacheControl) {
1285
+ let cacheControl = 'public';
1286
+
1287
+ if (opts.maxAge > 0) {
1288
+ cacheControl += `, max-age=${opts.maxAge}`;
1289
+ }
1290
+
1291
+ if (opts.immutable) {
1292
+ cacheControl += ', immutable';
1293
+ }
1294
+
1295
+ res.setHeader('Cache-Control', cacheControl);
1296
+ }
1297
+
1298
+ const mimeType = getMimeType(filePath);
1299
+ res.setHeader('Content-Type', mimeType);
1300
+ res.setHeader('Content-Length', stats.size);
1301
+
1302
+ if (typeof opts.setHeaders === 'function') {
1303
+ opts.setHeaders(res, filePath, stats);
1304
+ }
1305
+
1306
+ if (req.method === 'HEAD') {
1307
+ return res.end();
1308
+ }
1309
+
1310
+ const data = await fs.promises.readFile(filePath);
1311
+ res.end(data);
1312
+ return;
1313
+
1314
+ } catch (error) {
1315
+ console.error('Static middleware error:', error);
1316
+ if (opts.fallthrough) return next();
1317
+ res.status(500).send('Internal Server Error');
1318
+ }
1319
+ };
1320
+ }
1321
+
1000
1322
  _mountRouter(basePath, router) {
1001
1323
  basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
1002
1324
  router.groupStack = [...this.groupStack];
1003
1325
 
1004
1326
  router.routes.forEach(route => {
1005
- const fullPath = route.path === '/' ? basePath : basePath + route.path;
1327
+ const fullPath = route.path === '' ? basePath : basePath + route.path;
1006
1328
 
1007
1329
  this.routes.push({
1008
1330
  ...route,
@@ -1015,6 +1337,13 @@ class LiekoExpress {
1015
1337
  bodyParserOptions: router.bodyParserOptions
1016
1338
  });
1017
1339
  });
1340
+
1341
+ router.middlewares.forEach(mw => {
1342
+ this.middlewares.push({
1343
+ path: basePath === '' ? mw.path : (mw.path ? basePath + mw.path : basePath),
1344
+ handler: mw.handler
1345
+ });
1346
+ });
1018
1347
  }
1019
1348
 
1020
1349
  _addRoute(method, path, ...handlers) {
@@ -1031,43 +1360,94 @@ class LiekoExpress {
1031
1360
  }
1032
1361
  });
1033
1362
 
1034
- this.routes.push({
1035
- method,
1036
- path,
1037
- handler: finalHandler,
1038
- middlewares: routeMiddlewares,
1039
- pattern: this._pathToRegex(path),
1040
- groupChain: [...this.groupStack]
1363
+ const paths = Array.isArray(path) ? path : [path];
1364
+
1365
+ paths.forEach(original => {
1366
+ let p = String(original).trim();
1367
+ p = p.replace(/\/+/g, '/');
1368
+
1369
+ if (p !== '/' && p.endsWith('/')) {
1370
+ p = p.slice(0, -1);
1371
+ }
1372
+
1373
+ const exists = this.routes.some(r =>
1374
+ r.method === method &&
1375
+ r.path === p &&
1376
+ r.handler === finalHandler
1377
+ );
1378
+
1379
+ if (exists) return;
1380
+
1381
+ this.routes.push({
1382
+ method,
1383
+ path: p,
1384
+ originalPath: original,
1385
+ handler: finalHandler,
1386
+ handlerName: finalHandler.name || 'anonymous',
1387
+ middlewares: routeMiddlewares,
1388
+ pattern: this._pathToRegex(p),
1389
+ allowTrailingSlash: this.settings.allowTrailingSlash ?? false,
1390
+ groupChain: [...this.groupStack]
1391
+ });
1041
1392
  });
1042
1393
  }
1043
1394
 
1044
1395
  _pathToRegex(path) {
1045
- let pattern = path
1046
- .replace(/:(\w+)/g, '(?<$1>[^/]+)') // params :id
1047
- .replace(/\*/g, '.*'); // wildcards
1396
+ let p = String(path).trim();
1397
+ p = p.replace(/\/+/g, '/');
1398
+
1399
+ if (p !== '/' && p.endsWith('/')) {
1400
+ p = p.slice(0, -1);
1401
+ }
1402
+
1403
+ let pattern = p
1404
+ .replace(/:(\w+)/g, '(?<$1>[^/]+)')
1405
+ .replace(/\*/g, '.*');
1048
1406
 
1049
- const allowTrailing = this.settings.allowTrailingSlash === true ||
1050
- this.settings.strictTrailingSlash === false;
1407
+ const isStatic = !/[:*]/.test(p) && p !== '/';
1051
1408
 
1052
- const isStaticRoute = !pattern.includes('(') &&
1053
- !pattern.includes('*') &&
1054
- !path.endsWith('/');
1409
+ const allowTrailing = this.settings.allowTrailingSlash !== false;
1055
1410
 
1056
- if (allowTrailing && isStaticRoute) {
1411
+ if (isStatic && allowTrailing) {
1057
1412
  pattern += '/?';
1058
1413
  }
1059
1414
 
1415
+ if (p === '/') {
1416
+ return /^\/?$/;
1417
+ }
1418
+
1060
1419
  return new RegExp(`^${pattern}$`);
1061
1420
  }
1062
1421
 
1063
1422
  _findRoute(method, pathname) {
1064
1423
  for (const route of this.routes) {
1065
1424
  if (route.method !== method && route.method !== 'ALL') continue;
1425
+
1066
1426
  const match = pathname.match(route.pattern);
1067
1427
  if (match) {
1068
- return { ...route, params: match.groups || {} };
1428
+ return { ...route, params: match.groups || {}, matchedPath: pathname };
1429
+ }
1430
+ }
1431
+
1432
+ if (pathname.endsWith('/') && pathname.length > 1) {
1433
+ const cleanPath = pathname.slice(0, -1);
1434
+ for (const route of this.routes) {
1435
+ if (route.method !== method && route.method !== 'ALL') continue;
1436
+
1437
+ if (route.path === cleanPath && route.allowTrailingSlash !== false) {
1438
+ const match = cleanPath.match(route.pattern);
1439
+ if (match) {
1440
+ return {
1441
+ ...route,
1442
+ params: match.groups || {},
1443
+ matchedPath: cleanPath,
1444
+ wasTrailingSlash: true
1445
+ };
1446
+ }
1447
+ }
1069
1448
  }
1070
1449
  }
1450
+
1071
1451
  return null;
1072
1452
  }
1073
1453
 
@@ -1160,15 +1540,21 @@ class LiekoExpress {
1160
1540
 
1161
1541
  _parseIp(rawIp) {
1162
1542
  if (!rawIp) return { raw: null, ipv4: null, ipv6: null };
1543
+ let ip = rawIp.trim();
1163
1544
 
1164
- let ip = rawIp;
1545
+ if (ip === '::1') {
1546
+ ip = '127.0.0.1';
1547
+ }
1165
1548
 
1166
- // Remove IPv6 IPv4-mapped prefix "::ffff:"
1167
- if (ip.startsWith("::ffff:")) {
1168
- ip = ip.replace("::ffff:", "");
1549
+ if (ip.startsWith('::ffff:')) {
1550
+ ip = ip.slice(7);
1169
1551
  }
1170
1552
 
1171
- const family = net.isIP(ip); // 0=invalid, 4=IPv4, 6=IPv6
1553
+ const family = net.isIP(ip);
1554
+
1555
+ if (family === 0) {
1556
+ return { raw: rawIp, ipv4: null, ipv6: null };
1557
+ }
1172
1558
 
1173
1559
  return {
1174
1560
  raw: rawIp,
@@ -1229,8 +1615,9 @@ class LiekoExpress {
1229
1615
  req._startTime = process.hrtime.bigint();
1230
1616
  this._enhanceResponse(req, res);
1231
1617
 
1232
- try {
1618
+ req.originalUrl = url;
1233
1619
 
1620
+ try {
1234
1621
  if (req.method === "OPTIONS" && this.corsOptions.enabled) {
1235
1622
  this._applyCors(req, res, this.corsOptions);
1236
1623
  return;
@@ -1239,10 +1626,8 @@ class LiekoExpress {
1239
1626
  const route = this._findRoute(req.method, pathname);
1240
1627
 
1241
1628
  if (route) {
1242
-
1243
- if (route.cors === false) { }
1244
-
1245
- else if (route.cors) {
1629
+ if (route.cors === false) {
1630
+ } else if (route.cors) {
1246
1631
  const finalCors = {
1247
1632
  ...this.corsOptions,
1248
1633
  enabled: true,
@@ -1251,15 +1636,11 @@ class LiekoExpress {
1251
1636
 
1252
1637
  this._applyCors(req, res, finalCors);
1253
1638
  if (req.method === "OPTIONS") return;
1254
- }
1255
-
1256
- else if (this.corsOptions.enabled) {
1639
+ } else if (this.corsOptions.enabled) {
1257
1640
  this._applyCors(req, res, this.corsOptions);
1258
1641
  if (req.method === "OPTIONS") return;
1259
1642
  }
1260
-
1261
1643
  } else {
1262
-
1263
1644
  if (this.corsOptions.enabled) {
1264
1645
  this._applyCors(req, res, this.corsOptions);
1265
1646
  if (req.method === "OPTIONS") return;
@@ -1282,10 +1663,28 @@ class LiekoExpress {
1282
1663
  for (const mw of this.middlewares) {
1283
1664
  if (res.headersSent) return;
1284
1665
 
1285
- if (mw.path && !pathname.startsWith(mw.path)) continue;
1666
+ let shouldExecute = false;
1667
+ let pathToStrip = '';
1668
+
1669
+ if (mw.path === null) {
1670
+ shouldExecute = true;
1671
+ } else if (url.startsWith(mw.path)) {
1672
+ shouldExecute = true;
1673
+ pathToStrip = mw.path;
1674
+ }
1675
+
1676
+ if (!shouldExecute) continue;
1286
1677
 
1287
1678
  await new Promise((resolve, reject) => {
1679
+ const currentUrl = req.url;
1680
+
1681
+ if (pathToStrip) {
1682
+ req.url = url.substring(pathToStrip.length) || '/';
1683
+ }
1684
+
1288
1685
  const next = async (err) => {
1686
+ req.url = currentUrl;
1687
+
1289
1688
  if (err) {
1290
1689
  await this._runErrorHandlers(err, req, res);
1291
1690
  return resolve();
@@ -1342,25 +1741,31 @@ class LiekoExpress {
1342
1741
  }
1343
1742
 
1344
1743
  _enhanceRequest(req) {
1345
- const remoteIp = req.connection.remoteAddress || '';
1346
-
1347
- req.ips = [remoteIp];
1348
- let clientIp = remoteIp;
1744
+ req.app = this;
1745
+ let remoteIp = req.connection?.remoteAddress ||
1746
+ req.socket?.remoteAddress ||
1747
+ '';
1349
1748
 
1350
1749
  const forwardedFor = req.headers['x-forwarded-for'];
1750
+ let clientIp = remoteIp;
1751
+ let ipsChain = [remoteIp];
1351
1752
 
1352
1753
  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;
1754
+ const chain = forwardedFor
1755
+ .split(',')
1756
+ .map(s => s.trim())
1757
+ .filter(Boolean);
1758
+
1759
+ if (chain.length > 0 && this._isTrustedProxy(remoteIp)) {
1760
+ clientIp = chain[0];
1761
+ ipsChain = chain;
1358
1762
  }
1359
1763
  }
1360
1764
 
1361
1765
  req.ip = this._parseIp(clientIp);
1362
-
1363
- req.protocol = req.headers['x-forwarded-proto'] || 'http';
1766
+ req.ips = ipsChain;
1767
+ req.ip.display = req.ip.ipv4 ?? '127.0.0.1';
1768
+ req.protocol = (req.headers['x-forwarded-proto'] || 'http').split(',')[0].trim();
1364
1769
  req.secure = req.protocol === 'https';
1365
1770
 
1366
1771
  const host = req.headers['host'];
@@ -1378,6 +1783,7 @@ class LiekoExpress {
1378
1783
  }
1379
1784
 
1380
1785
  _enhanceResponse(req, res) {
1786
+ res.app = this;
1381
1787
  res.locals = {};
1382
1788
  let responseSent = false;
1383
1789
  let statusCode = 200;
@@ -1419,11 +1825,20 @@ class LiekoExpress {
1419
1825
  };
1420
1826
 
1421
1827
  const originalSetHeader = res.setHeader.bind(res);
1422
- res.setHeader = (name, value) => {
1828
+
1829
+ res.setHeader = function (name, value) {
1423
1830
  originalSetHeader(name, value);
1424
- return res;
1831
+ return this;
1832
+ };
1833
+
1834
+ res.set = function (name, value) {
1835
+ if (arguments.length === 1 && typeof name === 'object' && name !== null) {
1836
+ Object.entries(name).forEach(([k, v]) => originalSetHeader(k, v));
1837
+ } else {
1838
+ originalSetHeader(name, value);
1839
+ }
1840
+ return this;
1425
1841
  };
1426
- res.set = res.setHeader;
1427
1842
  res.header = res.setHeader;
1428
1843
 
1429
1844
  res.removeHeader = function (name) {
@@ -1436,6 +1851,85 @@ class LiekoExpress {
1436
1851
  return res;
1437
1852
  };
1438
1853
 
1854
+ res.render = async (view, options = {}, callback) => {
1855
+ if (responseSent) return res;
1856
+
1857
+ try {
1858
+ const locals = { ...res.locals, ...options };
1859
+ let viewPath = view;
1860
+ let ext = path.extname(view);
1861
+
1862
+ if (!ext) {
1863
+ ext = this.settings['view engine'];
1864
+ if (!ext) {
1865
+ throw new Error('No default view engine specified. Use app.set("view engine", "ejs") or provide file extension.');
1866
+ }
1867
+ if (!ext.startsWith('.')) ext = '.' + ext;
1868
+ viewPath = view + ext;
1869
+ }
1870
+
1871
+ const viewsDir = this.settings.views || path.join(process.cwd(), 'views');
1872
+ let fullPath = path.join(viewsDir, viewPath);
1873
+
1874
+ let fileExists = false;
1875
+ try {
1876
+ await fs.promises.access(fullPath);
1877
+ fileExists = true;
1878
+ } catch (err) {
1879
+ const htmlPath = fullPath.replace(new RegExp(ext.replace('.', '\\.') + '$'), '.html');
1880
+ try {
1881
+ await fs.promises.access(htmlPath);
1882
+ fullPath = htmlPath;
1883
+ fileExists = true;
1884
+ } catch (err2) {
1885
+ fileExists = false;
1886
+ }
1887
+ }
1888
+
1889
+ if (!fileExists) {
1890
+ const error = new Error(
1891
+ `View "${view}" not found. Tried:\n` +
1892
+ `- ${fullPath}\n` +
1893
+ `- ${fullPath.replace(new RegExp(ext.replace('.', '\\.') + '$'), '.html')}`
1894
+ );
1895
+ error.code = 'ENOENT';
1896
+ if (callback) return callback(error);
1897
+ throw error;
1898
+ }
1899
+
1900
+ const renderEngine = this.engines[ext];
1901
+
1902
+ if (!renderEngine) {
1903
+ throw new Error(`No engine registered for extension "${ext}". Use app.engine("${ext}", renderFunction)`);
1904
+ }
1905
+
1906
+ renderEngine(fullPath, locals, (err, html) => {
1907
+ if (err) {
1908
+ if (callback) return callback(err);
1909
+ throw err;
1910
+ }
1911
+
1912
+ if (callback) {
1913
+ callback(null, html);
1914
+ } else {
1915
+ res.statusCode = statusCode;
1916
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1917
+ responseSent = true;
1918
+ statusCode = 200;
1919
+ res.end(html);
1920
+ }
1921
+ });
1922
+
1923
+ } catch (error) {
1924
+ if (callback) {
1925
+ callback(error);
1926
+ } else {
1927
+ throw error;
1928
+ }
1929
+ }
1930
+ };
1931
+
1932
+
1439
1933
  res.json = (data) => {
1440
1934
  if (responseSent) return res;
1441
1935
 
@@ -1620,19 +2114,35 @@ class LiekoExpress {
1620
2114
  bodySizeFormatted = `${(bodySize / (1024 * 1024 * 1024)).toFixed(2)} GB`;
1621
2115
  }
1622
2116
 
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
- );
2117
+ const logLines = [
2118
+ '[DEBUG REQUEST]',
2119
+ `→ ${req.method} ${req.originalUrl}`,
2120
+ `→ IP: ${req.ip.ipv4 || '127.0.0.1'}`,
2121
+ `→ Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m`,
2122
+ `→ Duration: ${timeFormatted}`,
2123
+ ];
2124
+
2125
+ if (req.params && Object.keys(req.params).length > 0) {
2126
+ logLines.push(`→ Params: ${JSON.stringify(req.params)}`);
2127
+ }
2128
+
2129
+ if (req.query && Object.keys(req.query).length > 0) {
2130
+ logLines.push(`→ Query: ${JSON.stringify(req.query)}`);
2131
+ }
2132
+
2133
+ if (req.body && Object.keys(req.body).length > 0) {
2134
+ const bodyStr = JSON.stringify(req.body);
2135
+ const truncated = bodyStr.substring(0, 200) + (bodyStr.length > 200 ? '...' : '');
2136
+ logLines.push(`→ Body: ${truncated}`);
2137
+ logLines.push(`→ Body Size: ${bodySizeFormatted}`);
2138
+ }
2139
+
2140
+ if (req.files && Object.keys(req.files).length > 0) {
2141
+ logLines.push(`→ Files: ${Object.keys(req.files).join(', ')}`);
2142
+ }
2143
+
2144
+ logLines.push('---------------------------------------------');
2145
+ console.log('\n' + logLines.join('\n') + '\n');
1636
2146
  }
1637
2147
 
1638
2148
  _logCorsDebug(req, opts) {
@@ -1662,84 +2172,76 @@ class LiekoExpress {
1662
2172
  }
1663
2173
  }
1664
2174
 
1665
- _buildRouteTree() {
1666
- const tree = {};
1667
-
1668
- for (const route of this.routes) {
1669
- let node = tree;
2175
+ listRoutes() {
2176
+ const routeEntries = [];
1670
2177
 
1671
- for (const group of route.groupChain) {
1672
- const key = group.basePath;
2178
+ this.routes.forEach(route => {
2179
+ const existing = routeEntries.find(
2180
+ entry => entry.method === route.method &&
2181
+ entry.handler === route.handler
2182
+ );
1673
2183
 
1674
- if (!node[key]) {
1675
- node[key] = {
1676
- __meta: group,
1677
- __children: {},
1678
- __routes: []
1679
- };
2184
+ if (existing) {
2185
+ if (!Array.isArray(existing.path)) {
2186
+ existing.path = [existing.path];
1680
2187
  }
1681
-
1682
- node = node[key].__children;
2188
+ existing.path.push(route.path);
2189
+ } else {
2190
+ routeEntries.push({
2191
+ method: route.method,
2192
+ path: route.path,
2193
+ middlewares: route.middlewares.length,
2194
+ handler: route.handler
2195
+ });
1683
2196
  }
2197
+ });
1684
2198
 
1685
- if (!node.__routes) node.__routes = [];
1686
- node.__routes.push(route);
1687
- }
1688
-
1689
- return tree;
1690
- }
1691
-
1692
- listRoutes() {
1693
- return this.routes.map(route => ({
1694
- method: route.method,
1695
- path: route.path,
1696
- middlewares: route.middlewares.length
2199
+ return routeEntries.map(entry => ({
2200
+ method: entry.method,
2201
+ path: entry.path,
2202
+ middlewares: entry.middlewares
1697
2203
  }));
1698
2204
  }
1699
2205
 
1700
2206
  printRoutes() {
1701
- console.log('\n📌 Registered Routes:\n');
1702
-
1703
- this.routes.forEach(r => {
1704
- console.log(
1705
- `${r.method.padEnd(6)} ${r.path} ` +
1706
- `(mw: ${r.middlewares.length})`
1707
- );
1708
- });
1709
-
1710
- console.log('');
1711
- }
1712
-
1713
- printRoutesNested(tree = null, indent = '') {
1714
- if (!tree) {
1715
- console.log('\n📌 Nested Routes:\n');
1716
- tree = this._buildRouteTree();
2207
+ if (this.routes.length === 0) {
2208
+ console.log('\nNo routes registered.\n');
2209
+ return;
1717
2210
  }
1718
2211
 
1719
- const prefix = indent + ' ';
1720
-
1721
- for (const [path, data] of Object.entries(tree)) {
1722
- if (path.startsWith("__")) continue;
2212
+ console.log(`\nRegistered Routes: ${this.routes.length}\n`);
1723
2213
 
1724
- const mwCount = data.__meta.middlewares.length;
1725
- console.log(`${indent}${path} [${mwCount} mw]`);
2214
+ const grouped = new Map();
1726
2215
 
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
- }
2216
+ for (const route of this.routes) {
2217
+ const key = `${route.method}|${route.handler}`;
2218
+ if (!grouped.has(key)) {
2219
+ grouped.set(key, {
2220
+ method: route.method,
2221
+ paths: [],
2222
+ mw: route.middlewares.length
2223
+ });
2224
+ }
2225
+ const entry = grouped.get(key);
2226
+ const p = route.path || '/';
2227
+ if (!entry.paths.includes(p)) {
2228
+ entry.paths.push(p);
1732
2229
  }
1733
- this.printRoutesNested(data.__children, prefix);
1734
2230
  }
1735
2231
 
1736
- if (tree.__routes) {
1737
- for (const route of tree.__routes) {
1738
- console.log(`${indent}${route.method.padEnd(6)} ${route.path}`);
1739
- }
2232
+ const sorted = Array.from(grouped.values()).sort((a, b) => {
2233
+ if (a.method !== b.method) return a.method.localeCompare(b.method);
2234
+ return a.paths[0].localeCompare(b.paths[0]);
2235
+ });
2236
+
2237
+ for (const r of sorted) {
2238
+ const pathStr = r.paths.length === 1
2239
+ ? r.paths[0]
2240
+ : r.paths.join(', ');
2241
+
2242
+ console.log(` \x1b[36m${r.method.padEnd(7)}\x1b[0m \x1b[33m${pathStr}\x1b[0m \x1b[90m(mw: ${r.mw})\x1b[0m`);
1740
2243
  }
1741
2244
  }
1742
-
1743
2245
  listen() {
1744
2246
  const args = Array.from(arguments);
1745
2247
  const server = createServer(this._handleRequest.bind(this));
@@ -1766,3 +2268,7 @@ module.exports.validators = validators;
1766
2268
  module.exports.validate = validate;
1767
2269
  module.exports.validatePartial = validatePartial;
1768
2270
  module.exports.ValidationError = ValidationError;
2271
+ module.exports.static = function (root, options) {
2272
+ const app = new LiekoExpress();
2273
+ return app.static(root, options);
2274
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lieko-express",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/eiwSrvt/lieko-express"