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 +3 -5
- package/lieko-express.d.ts +96 -67
- package/lieko-express.js +642 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# **Lieko-express — A Modern, Minimal,
|
|
1
|
+
# **Lieko-express — A Modern, Minimal, express-like Framework for Node.js**
|
|
2
2
|
|
|
3
|
-
A lightweight, fast, and modern Node.js
|
|
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
|

|
|
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)
|
|
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)
|
package/lieko-express.d.ts
CHANGED
|
@@ -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[],
|
|
84
|
-
|
|
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) =>
|
|
136
|
-
|
|
137
|
-
interface
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
178
|
+
function validate(schema: Schema): Handler;
|
|
179
|
+
|
|
180
|
+
function validatePartial(schema: Schema): Handler;
|
|
154
181
|
|
|
155
182
|
// -------------- Routes / Options --------------
|
|
156
|
-
interface
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
maybeHandler?: Handler<ExtractRouteParams<Path>, Q, B>
|
|
217
|
+
...handlers: Handler<ExtractRouteParams<Path>, Q, B>[]
|
|
197
218
|
): this;
|
|
198
219
|
|
|
199
|
-
|
|
220
|
+
all<Path extends string, Q = any, B = any>(
|
|
200
221
|
path: Path,
|
|
201
|
-
|
|
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(
|
|
230
|
+
group(basePath: string, callback: (group: App) => void): this;
|
|
220
231
|
|
|
221
232
|
// CORS
|
|
222
|
-
cors(options?: Partial<CorsOptions>):
|
|
233
|
+
cors(options?: Partial<CorsOptions>): Handler;
|
|
223
234
|
|
|
224
|
-
// body parser
|
|
225
|
-
bodyParser
|
|
235
|
+
// body parser
|
|
236
|
+
bodyParser: {
|
|
237
|
+
json(options?: JsonBodyOptions): Handler;
|
|
238
|
+
urlencoded(options?: UrlencodedOptions): Handler;
|
|
239
|
+
multipart(options?: MultipartOptions): Handler;
|
|
240
|
+
};
|
|
226
241
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
validate(schemaOrDef: string | Schema | SchemaDefinition): Handler;
|
|
242
|
+
// static files
|
|
243
|
+
static(root: string, options?: { maxAge?: number; index?: string }): Handler;
|
|
230
244
|
|
|
231
|
-
// error
|
|
232
|
-
|
|
233
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
554
|
-
|
|
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;
|
|
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(
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
912
|
-
|
|
966
|
+
const firstLine = funcString.split('\n')[0];
|
|
967
|
+
const secondLine = funcString.split('\n')[1] || '';
|
|
968
|
+
const thirdLine = funcString.split('\n')[2] || '';
|
|
913
969
|
|
|
914
|
-
|
|
915
|
-
|
|
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 === '
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
|
1050
|
-
this.settings.strictTrailingSlash === false;
|
|
1407
|
+
const isStatic = !/[:*]/.test(p) && p !== '/';
|
|
1051
1408
|
|
|
1052
|
-
const
|
|
1053
|
-
!pattern.includes('*') &&
|
|
1054
|
-
!path.endsWith('/');
|
|
1409
|
+
const allowTrailing = this.settings.allowTrailingSlash !== false;
|
|
1055
1410
|
|
|
1056
|
-
if (
|
|
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
|
-
|
|
1545
|
+
if (ip === '::1') {
|
|
1546
|
+
ip = '127.0.0.1';
|
|
1547
|
+
}
|
|
1165
1548
|
|
|
1166
|
-
|
|
1167
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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.
|
|
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
|
-
|
|
1828
|
+
|
|
1829
|
+
res.setHeader = function (name, value) {
|
|
1423
1830
|
originalSetHeader(name, value);
|
|
1424
|
-
return
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
for (const route of this.routes) {
|
|
1669
|
-
let node = tree;
|
|
2175
|
+
listRoutes() {
|
|
2176
|
+
const routeEntries = [];
|
|
1670
2177
|
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
__children: {},
|
|
1678
|
-
__routes: []
|
|
1679
|
-
};
|
|
2184
|
+
if (existing) {
|
|
2185
|
+
if (!Array.isArray(existing.path)) {
|
|
2186
|
+
existing.path = [existing.path];
|
|
1680
2187
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
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
|
-
|
|
1686
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1725
|
-
console.log(`${indent}${path} [${mwCount} mw]`);
|
|
2214
|
+
const grouped = new Map();
|
|
1726
2215
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
+
};
|