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