lieko-express 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lieko-express.js CHANGED
@@ -3,410 +3,15 @@ const net = require("net");
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
 
6
- process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
7
-
8
- class ValidationError extends Error {
9
- constructor(errors) {
10
- super('Validation failed');
11
- this.name = 'ValidationError';
12
- this.errors = errors;
13
- }
14
- }
15
-
16
- class Schema {
17
- constructor(rules) {
18
- this.rules = rules;
19
- this.fields = rules;
20
- }
21
-
22
- validate(data) {
23
- const errors = [];
24
- for (const [field, validators] of Object.entries(this.rules)) {
25
- const value = data[field];
26
- for (const validator of validators) {
27
- const error = validator(value, field, data);
28
- if (error) {
29
- errors.push(error);
30
- break;
31
- }
32
- }
33
- }
34
- if (errors.length > 0) throw new ValidationError(errors);
35
- return true;
36
- }
37
- }
38
-
39
- const validators = {
40
- required: (message = 'Field is required') => {
41
- return (value, field) => {
42
- if (value === undefined || value === null || value === '') {
43
- return { field, message, type: 'required' };
44
- }
45
- return null;
46
- };
47
- },
48
-
49
- requiredTrue: (message = 'Field must be true') => {
50
- return (value, field) => {
51
- const normalized = value === true || value === 'true' || value === '1' || value === 1;
52
- if (!normalized) {
53
- return { field, message, type: 'requiredTrue' };
54
- }
55
- return null;
56
- }
57
- },
58
-
59
- optional: () => {
60
- return () => null;
61
- },
62
-
63
- string: (message = 'Field must be a string') => {
64
- return (value, field) => {
65
- if (value !== undefined && typeof value !== 'string') {
66
- return { field, message, type: 'string' };
67
- }
68
- return null;
69
- };
70
- },
71
-
72
- number: (message = 'Field must be a number') => {
73
- return (value, field) => {
74
- if (value !== undefined && typeof value !== 'number') {
75
- return { field, message, type: 'number' };
76
- }
77
- return null;
78
- };
79
- },
80
-
81
- boolean: (message = 'Field must be a boolean') => {
82
- return (value, field) => {
83
- if (value === undefined || value === null || value === '') return null;
84
-
85
- const validTrue = ['true', true, 1, '1'];
86
- const validFalse = ['false', false, 0, '0'];
87
-
88
- const isValid = validTrue.includes(value) || validFalse.includes(value);
89
-
90
- if (!isValid) {
91
- return { field, message, type: 'boolean' };
92
- }
93
-
94
- return null;
95
- };
96
- },
97
-
98
- integer: (message = 'Field must be an integer') => {
99
- return (value, field) => {
100
- if (value !== undefined && !Number.isInteger(value)) {
101
- return { field, message, type: 'integer' };
102
- }
103
- return null;
104
- };
105
- },
106
-
107
- positive: (message = 'Field must be positive') => {
108
- return (value, field) => {
109
- if (value !== undefined && value <= 0) {
110
- return { field, message, type: 'positive' };
111
- }
112
- return null;
113
- };
114
- },
115
-
116
- negative: (message = 'Field must be negative') => {
117
- return (value, field) => {
118
- if (value !== undefined && value >= 0) {
119
- return { field, message, type: 'negative' };
120
- }
121
- return null;
122
- };
123
- },
124
-
125
- email: (message = 'Invalid email format') => {
126
- return (value, field) => {
127
- if (value !== undefined && value !== null) {
128
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
129
- if (!emailRegex.test(value)) {
130
- return { field, message, type: 'email' };
131
- }
132
- }
133
- return null;
134
- };
135
- },
136
-
137
- min: (minValue, message) => {
138
- return (value, field) => {
139
- if (value !== undefined && value !== null) {
140
- if (typeof value === 'string' && value.length < minValue) {
141
- return {
142
- field,
143
- message: message || `Field must be at least ${minValue} characters`,
144
- type: 'min'
145
- };
146
- }
147
- if (typeof value === 'number' && value < minValue) {
148
- return {
149
- field,
150
- message: message || `Field must be at least ${minValue}`,
151
- type: 'min'
152
- };
153
- }
154
- }
155
- return null;
156
- };
157
- },
158
-
159
- max: (maxValue, message) => {
160
- return (value, field) => {
161
- if (value !== undefined && value !== null) {
162
- if (typeof value === 'string' && value.length > maxValue) {
163
- return {
164
- field,
165
- message: message || `Field must be at most ${maxValue} characters`,
166
- type: 'max'
167
- };
168
- }
169
- if (typeof value === 'number' && value > maxValue) {
170
- return {
171
- field,
172
- message: message || `Field must be at most ${maxValue}`,
173
- type: 'max'
174
- };
175
- }
176
- }
177
- return null;
178
- };
179
- },
180
-
181
- length: (n, message) => {
182
- return (value, field) => {
183
- if (typeof value === 'string' && value.length !== n) {
184
- return {
185
- field,
186
- message: message || `Field must be exactly ${n} characters`,
187
- type: 'length'
188
- };
189
- }
190
- return null;
191
- };
192
- },
193
-
194
- minLength: (minLength, message) => {
195
- return (value, field) => {
196
- if (value !== undefined && value !== null && typeof value === 'string') {
197
- if (value.length < minLength) {
198
- return {
199
- field,
200
- message: message || `Field must be at least ${minLength} characters`,
201
- type: 'minLength'
202
- };
203
- }
204
- }
205
- return null;
206
- };
207
- },
208
-
209
- maxLength: (maxLength, message) => {
210
- return (value, field) => {
211
- if (value !== undefined && value !== null && typeof value === 'string') {
212
- if (value.length > maxLength) {
213
- return {
214
- field,
215
- message: message || `Field must be at most ${maxLength} characters`,
216
- type: 'maxLength'
217
- };
218
- }
219
- }
220
- return null;
221
- };
222
- },
6
+ const {
7
+ Schema,
8
+ ValidationError,
9
+ validators,
10
+ validate,
11
+ validatePartial
12
+ } = require('./lib/schema');
223
13
 
224
- pattern: (regex, message = 'Invalid format') => {
225
- return (value, field) => {
226
- if (value !== undefined && value !== null && typeof value === 'string') {
227
- if (!regex.test(value)) {
228
- return { field, message, type: 'pattern' };
229
- }
230
- }
231
- return null;
232
- };
233
- },
234
-
235
- oneOf: (allowedValues, message) => {
236
- return (value, field) => {
237
- if (value !== undefined && value !== null) {
238
- if (!allowedValues.includes(value)) {
239
- return {
240
- field,
241
- message: message || `Field must be one of: ${allowedValues.join(', ')}`,
242
- type: 'oneOf'
243
- };
244
- }
245
- }
246
- return null;
247
- };
248
- },
249
-
250
- notOneOf: (values, message) => {
251
- return (value, field) => {
252
- if (values.includes(value)) {
253
- return {
254
- field,
255
- message: message || `Field cannot be one of: ${values.join(', ')}`,
256
- type: 'notOneOf'
257
- };
258
- }
259
- return null;
260
- };
261
- },
262
-
263
- custom: (validatorFn, message = 'Validation failed') => {
264
- return (value, field, data) => {
265
- const isValid = validatorFn(value, data);
266
- if (!isValid) {
267
- return { field, message, type: 'custom' };
268
- }
269
- return null;
270
- };
271
- },
272
-
273
- equal: (expectedValue, message) => {
274
- return (value, field) => {
275
- if (value !== expectedValue) {
276
- return {
277
- field,
278
- message: message || `Field must be equal to ${expectedValue}`,
279
- type: 'equal'
280
- };
281
- }
282
- return null;
283
- };
284
- },
285
-
286
- mustBeTrue: (message = 'This field must be accepted') => {
287
- return (value, field) => {
288
- const normalized = value === true || value === 'true' || value === '1' || value === 1;
289
- if (!normalized) {
290
- return { field, message, type: 'mustBeTrue' };
291
- }
292
- return null;
293
- };
294
- },
295
-
296
- mustBeFalse: (message = 'This field must be declined') => {
297
- return (value, field) => {
298
- const normalized = value === false || value === 'false' || value === '0' || value === 0;
299
- if (!normalized) {
300
- return { field, message, type: 'mustBeFalse' };
301
- }
302
- return null;
303
- };
304
- },
305
-
306
- date: (message = 'Invalid date') => {
307
- return (value, field) => {
308
- if (!value) return null;
309
- const date = new Date(value);
310
- if (isNaN(date.getTime())) {
311
- return { field, message, type: 'date' };
312
- }
313
- return null;
314
- };
315
- },
316
-
317
- before: (limit, message) => {
318
- return (value, field) => {
319
- if (!value) return null;
320
- const d1 = new Date(value);
321
- const d2 = new Date(limit);
322
- if (isNaN(d1) || d1 >= d2) {
323
- return {
324
- field,
325
- message: message || `Date must be before ${limit}`,
326
- type: 'before'
327
- };
328
- }
329
- return null;
330
- };
331
- },
332
-
333
- after: (limit, message) => {
334
- return (value, field) => {
335
- if (!value) return null;
336
- const d1 = new Date(value);
337
- const d2 = new Date(limit);
338
- if (isNaN(d1) || d1 <= d2) {
339
- return {
340
- field,
341
- message: message || `Date must be after ${limit}`,
342
- type: 'after'
343
- };
344
- }
345
- return null;
346
- };
347
- },
348
-
349
- startsWith: (prefix, message) => {
350
- return (value, field) => {
351
- if (typeof value === 'string' && !value.startsWith(prefix)) {
352
- return {
353
- field,
354
- message: message || `Field must start with "${prefix}"`,
355
- type: 'startsWith'
356
- };
357
- }
358
- return null;
359
- };
360
- },
361
-
362
- endsWith: (suffix, message) => {
363
- return (value, field) => {
364
- if (typeof value === 'string' && !value.endsWith(suffix)) {
365
- return {
366
- field,
367
- message: message || `Field must end with "${suffix}"`,
368
- type: 'endsWith'
369
- };
370
- }
371
- return null;
372
- };
373
- }
374
- };
375
-
376
- function validate(schema) {
377
- return (req, res, next) => {
378
- try {
379
- schema.validate(req.body);
380
- next();
381
- } catch (error) {
382
- if (error instanceof ValidationError) {
383
- return res.status(400).json({
384
- success: false,
385
- message: 'Validation failed',
386
- errors: error.errors
387
- });
388
- }
389
- throw error;
390
- }
391
- };
392
- }
393
-
394
- function validatePartial(schema) {
395
- const partial = {};
396
-
397
- for (const field in schema.fields) {
398
- const rules = schema.fields[field];
399
-
400
- const filtered = rules.filter(v =>
401
- v.name !== 'required' &&
402
- v.name !== 'requiredTrue' &&
403
- v.name !== 'mustBeTrue'
404
- );
405
- partial[field] = [validators.optional(), ...filtered];
406
- }
407
-
408
- return new Schema(partial);
409
- }
14
+ process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
410
15
 
411
16
  class LiekoExpress {
412
17
  constructor() {
@@ -424,10 +29,11 @@ class LiekoExpress {
424
29
  strictTrailingSlash: true,
425
30
  allowTrailingSlash: true,
426
31
  views: path.join(process.cwd(), "views"),
427
- "view engine": null
32
+ "view engine": "html"
428
33
  };
429
34
 
430
35
  this.engines = {};
36
+ this.engines['.html'] = this._defaultHtmlEngine.bind(this);
431
37
 
432
38
  this.bodyParserOptions = {
433
39
  json: {
@@ -458,97 +64,20 @@ class LiekoExpress {
458
64
  }
459
65
 
460
66
  cors(options = {}) {
461
- this.corsOptions = {
462
- ...this.corsOptions,
463
- enabled: true,
464
- ...options
465
- };
466
- }
467
-
468
- _matchOrigin(origin, allowedOrigin) {
469
- if (!origin || !allowedOrigin) return false;
470
-
471
- if (Array.isArray(allowedOrigin)) {
472
- return allowedOrigin.some(o => this._matchOrigin(origin, o));
473
- }
474
-
475
- if (allowedOrigin === "*") return true;
476
-
477
- // Wildcard https://*.example.com
478
- if (allowedOrigin.includes("*")) {
479
- const regex = new RegExp("^" + allowedOrigin
480
- .replace(/\./g, "\\.")
481
- .replace(/\*/g, ".*") + "$");
482
-
483
- return regex.test(origin);
484
- }
485
-
486
- return origin === allowedOrigin;
487
- }
488
-
489
- _applyCors(req, res, opts) {
490
- if (!opts || !opts.enabled) return;
491
-
492
- const requestOrigin = req.headers.origin || "";
493
-
494
- let finalOrigin = "*";
495
-
496
- if (opts.strictOrigin && requestOrigin) {
497
- const allowed = this._matchOrigin(requestOrigin, opts.origin);
498
-
499
- if (!allowed) {
500
- res.statusCode = 403;
501
- return res.end(JSON.stringify({
502
- success: false,
503
- error: "Origin Forbidden",
504
- message: `Origin "${requestOrigin}" is not allowed`
505
- }));
506
- }
507
- }
508
-
509
- if (opts.origin === "*") {
510
- finalOrigin = "*";
511
- } else if (Array.isArray(opts.origin)) {
512
- const match = opts.origin.find(o => this._matchOrigin(requestOrigin, o));
513
- finalOrigin = match || opts.origin[0];
514
- } else {
515
- finalOrigin = this._matchOrigin(requestOrigin, opts.origin)
516
- ? requestOrigin
517
- : opts.origin;
518
- }
519
-
520
- this._logCorsDebug(req, {
521
- ...opts,
522
- origin: finalOrigin
523
- });
524
-
525
- res.setHeader("Access-Control-Allow-Origin", finalOrigin);
526
-
527
- if (opts.credentials) {
528
- res.setHeader("Access-Control-Allow-Credentials", "true");
67
+ if (options === false) {
68
+ this._corsMiddleware = null;
69
+ return this;
529
70
  }
530
71
 
531
- if (opts.exposedHeaders?.length) {
532
- res.setHeader("Access-Control-Expose-Headers",
533
- opts.exposedHeaders.join(", "));
534
- }
72
+ const middleware = require('./lib/cors')(options);
73
+ this._corsMiddleware = middleware;
535
74
 
536
- // Chrome Private Network Access
537
- if (
538
- opts.allowPrivateNetwork &&
539
- req.headers["access-control-request-private-network"] === "true"
540
- ) {
541
- res.setHeader("Access-Control-Allow-Private-Network", "true");
75
+ const stack = new Error().stack;
76
+ if (!stack.includes('at LiekoExpress.use')) {
77
+ this.use(middleware);
542
78
  }
543
79
 
544
- if (req.method === "OPTIONS") {
545
- res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
546
- res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
547
- res.setHeader("Access-Control-Max-Age", opts.maxAge);
548
-
549
- res.statusCode = 204;
550
- return res.end();
551
- }
80
+ return middleware;
552
81
  }
553
82
 
554
83
  debug(value = true) {
@@ -1075,248 +604,7 @@ ${cyan} (req, res, next) => {
1075
604
  }
1076
605
 
1077
606
  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
- };
607
+ return require('./lib/static')(root, options);
1320
608
  }
1321
609
 
1322
610
  _mountRouter(basePath, router) {
@@ -1352,6 +640,10 @@ ${cyan} (req, res, next) => {
1352
640
  }
1353
641
 
1354
642
  const finalHandler = handlers[handlers.length - 1];
643
+ if (!finalHandler) {
644
+ throw new Error(`Route handler is undefined for ${method} ${path}`);
645
+ }
646
+
1355
647
  const routeMiddlewares = handlers.slice(0, -1);
1356
648
 
1357
649
  routeMiddlewares.forEach(mw => {
@@ -1363,7 +655,7 @@ ${cyan} (req, res, next) => {
1363
655
  const paths = Array.isArray(path) ? path : [path];
1364
656
 
1365
657
  paths.forEach(original => {
1366
- let p = String(original).trim();
658
+ let p = String(original).trim();
1367
659
  p = p.replace(/\/+/g, '/');
1368
660
 
1369
661
  if (p !== '/' && p.endsWith('/')) {
@@ -1383,7 +675,7 @@ ${cyan} (req, res, next) => {
1383
675
  path: p,
1384
676
  originalPath: original,
1385
677
  handler: finalHandler,
1386
- handlerName: finalHandler.name || 'anonymous',
678
+ handlerName: (finalHandler && finalHandler.name) || 'anonymous',
1387
679
  middlewares: routeMiddlewares,
1388
680
  pattern: this._pathToRegex(p),
1389
681
  allowTrailingSlash: this.settings.allowTrailingSlash ?? false,
@@ -1447,7 +739,6 @@ ${cyan} (req, res, next) => {
1447
739
  }
1448
740
  }
1449
741
  }
1450
-
1451
742
  return null;
1452
743
  }
1453
744
 
@@ -1780,6 +1071,100 @@ ${cyan} (req, res, next) => {
1780
1071
 
1781
1072
  req.originalUrl = req.url;
1782
1073
  req.xhr = (req.headers['x-requested-with'] || '').toLowerCase() === 'xmlhttprequest';
1074
+
1075
+ req.get = (name) => {
1076
+ if (typeof name !== 'string') return undefined;
1077
+ const lower = name.toLowerCase();
1078
+ for (const key in req.headers) {
1079
+ if (key.toLowerCase() === lower) return req.headers[key];
1080
+ }
1081
+ return undefined;
1082
+ };
1083
+ req.header = req.get;
1084
+
1085
+ const parseAccept = (header) => {
1086
+ if (!header) return [];
1087
+ return header
1088
+ .split(',')
1089
+ .map(part => {
1090
+ const [type, ...rest] = part.trim().split(';');
1091
+ const q = rest
1092
+ .find(p => p.trim().startsWith('q='))
1093
+ ?.split('=')[1];
1094
+ const quality = q ? parseFloat(q) : 1.0;
1095
+ return { type: type.trim().toLowerCase(), quality };
1096
+ })
1097
+ .filter(item => item.quality > 0)
1098
+ .sort((a, b) => b.quality - a.quality)
1099
+ .map(item => item.type);
1100
+ };
1101
+
1102
+ const accepts = (types) => {
1103
+ if (!Array.isArray(types)) types = [types];
1104
+ const accepted = parseAccept(req.headers['accept']);
1105
+
1106
+ for (const type of types) {
1107
+ const t = type.toLowerCase();
1108
+
1109
+ if (accepted.includes(t)) return type;
1110
+
1111
+ if (accepted.some(a => {
1112
+ if (a === '*/*') return true;
1113
+ if (a.endsWith('/*')) {
1114
+ const prefix = a.slice(0, -1);
1115
+ return t.startsWith(prefix);
1116
+ }
1117
+ return false;
1118
+ })) {
1119
+ return type;
1120
+ }
1121
+ }
1122
+
1123
+ return false;
1124
+ };
1125
+
1126
+ req.accepts = function (types) {
1127
+ return accepts(types);
1128
+ };
1129
+
1130
+ req.acceptsLanguages = function (langs) {
1131
+ if (!Array.isArray(langs)) langs = [langs];
1132
+ const accepted = parseAccept(req.headers['accept-language'] || '');
1133
+ for (const lang of langs) {
1134
+ const l = lang.toLowerCase();
1135
+ if (accepted.some(a => a === l || a.startsWith(l + '-'))) return lang;
1136
+ }
1137
+ return false;
1138
+ };
1139
+
1140
+ req.acceptsEncodings = function (encodings) {
1141
+ if (!Array.isArray(encodings)) encodings = [encodings];
1142
+ const accepted = parseAccept(req.headers['accept-encoding'] || '');
1143
+ for (const enc of encodings) {
1144
+ if (accepted.includes(enc.toLowerCase())) return enc;
1145
+ }
1146
+ return false;
1147
+ };
1148
+
1149
+ req.acceptsCharsets = function (charsets) {
1150
+ if (!Array.isArray(charsets)) charsets = [charsets];
1151
+ const accepted = parseAccept(req.headers['accept-charset'] || '');
1152
+ for (const charset of charsets) {
1153
+ if (accepted.includes(charset.toLowerCase())) return charset;
1154
+ }
1155
+ return false;
1156
+ };
1157
+
1158
+ req.is = function (type) {
1159
+ const ct = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
1160
+ if (!type) return ct;
1161
+ const t = type.toLowerCase();
1162
+ if (t.includes('/')) return ct === t;
1163
+ if (t === 'json') return ct.includes('json');
1164
+ if (t === 'urlencoded') return ct.includes('x-www-form-urlencoded');
1165
+ if (t === 'multipart') return ct.includes('multipart');
1166
+ return false;
1167
+ };
1783
1168
  }
1784
1169
 
1785
1170
  _enhanceResponse(req, res) {
@@ -1858,39 +1243,46 @@ ${cyan} (req, res, next) => {
1858
1243
  const locals = { ...res.locals, ...options };
1859
1244
  let viewPath = view;
1860
1245
  let ext = path.extname(view);
1246
+ console.log("EXT: ", ext)
1861
1247
 
1862
1248
  if (!ext) {
1863
1249
  ext = this.settings['view engine'];
1864
1250
  if (!ext) {
1865
- throw new Error('No default view engine specified. Use app.set("view engine", "ejs") or provide file extension.');
1251
+ ext = '.html';
1252
+ viewPath = view + ext;
1253
+ } else {
1254
+ if (!ext.startsWith('.')) ext = '.' + ext;
1255
+ viewPath = view + ext;
1866
1256
  }
1867
- if (!ext.startsWith('.')) ext = '.' + ext;
1868
- viewPath = view + ext;
1869
1257
  }
1870
1258
 
1871
1259
  const viewsDir = this.settings.views || path.join(process.cwd(), 'views');
1872
1260
  let fullPath = path.join(viewsDir, viewPath);
1873
-
1261
+ console.log(fullPath)
1874
1262
  let fileExists = false;
1875
1263
  try {
1876
1264
  await fs.promises.access(fullPath);
1877
1265
  fileExists = true;
1878
1266
  } 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;
1267
+ console.log(err)
1268
+ const extensions = ['.html', '.ejs', '.pug', '.hbs'];
1269
+ for (const tryExt of extensions) {
1270
+ if (tryExt === ext) continue;
1271
+ const tryPath = fullPath.replace(new RegExp(ext.replace('.', '\\.') + '$'), tryExt);
1272
+ try {
1273
+ await fs.promises.access(tryPath);
1274
+ fullPath = tryPath;
1275
+ ext = tryExt;
1276
+ fileExists = true;
1277
+ break;
1278
+ } catch (err2) { }
1886
1279
  }
1887
1280
  }
1888
1281
 
1889
1282
  if (!fileExists) {
1890
1283
  const error = new Error(
1891
- `View "${view}" not found. Tried:\n` +
1892
- `- ${fullPath}\n` +
1893
- `- ${fullPath.replace(new RegExp(ext.replace('.', '\\.') + '$'), '.html')}`
1284
+ `View "${view}" not found in views directory "${viewsDir}".\n` +
1285
+ `Tried: ${fullPath}`
1894
1286
  );
1895
1287
  error.code = 'ENOENT';
1896
1288
  if (callback) return callback(error);
@@ -1900,24 +1292,38 @@ ${cyan} (req, res, next) => {
1900
1292
  const renderEngine = this.engines[ext];
1901
1293
 
1902
1294
  if (!renderEngine) {
1903
- throw new Error(`No engine registered for extension "${ext}". Use app.engine("${ext}", renderFunction)`);
1295
+ throw new Error(
1296
+ `No engine registered for extension "${ext}".\n` +
1297
+ `Use app.engine("${ext}", renderFunction) to register one.`
1298
+ );
1904
1299
  }
1905
1300
 
1906
- renderEngine(fullPath, locals, (err, html) => {
1907
- if (err) {
1908
- if (callback) return callback(err);
1909
- throw err;
1910
- }
1301
+ return new Promise((resolve, reject) => {
1302
+ renderEngine(fullPath, locals, (err, html) => {
1303
+ if (err) {
1304
+ if (callback) {
1305
+ callback(err);
1306
+ resolve();
1307
+ } else {
1308
+ reject(err);
1309
+ }
1310
+ return;
1311
+ }
1911
1312
 
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
- }
1313
+ if (callback) {
1314
+ callback(null, html);
1315
+ resolve();
1316
+ } else {
1317
+ /*
1318
+ HBS cause error header already sent here ??
1319
+ */
1320
+ //res.statusCode = statusCode || 200;
1321
+ //res.setHeader('Content-Type', 'text/html; charset=utf-8');
1322
+ //responseSent = true;
1323
+ res.html(html);
1324
+ resolve();
1325
+ }
1326
+ });
1921
1327
  });
1922
1328
 
1923
1329
  } catch (error) {
@@ -1929,17 +1335,15 @@ ${cyan} (req, res, next) => {
1929
1335
  }
1930
1336
  };
1931
1337
 
1932
-
1933
1338
  res.json = (data) => {
1934
1339
  if (responseSent) return res;
1935
1340
 
1936
1341
  const json = JSON.stringify(data);
1937
1342
  const length = Buffer.byteLength(json);
1938
1343
 
1939
- res.writeHead(statusCode, buildHeaders('application/json; charset=utf-8', length));
1344
+ res.writeHead(statusCode || 200, buildHeaders('application/json; charset=utf-8', length));
1940
1345
 
1941
1346
  responseSent = true;
1942
- statusCode = 200;
1943
1347
  return res.end(json);
1944
1348
  };
1945
1349
 
@@ -1964,15 +1368,14 @@ ${cyan} (req, res, next) => {
1964
1368
 
1965
1369
  const length = Buffer.byteLength(body);
1966
1370
 
1967
- res.writeHead(statusCode, buildHeaders(contentType, length));
1371
+ res.writeHead(statusCode || 200, buildHeaders(contentType, length));
1968
1372
 
1969
1373
  responseSent = true;
1970
- statusCode = 200;
1971
1374
  return res.end(body);
1972
1375
  };
1973
1376
 
1974
- res.html = function (html, status = 200) {
1975
- res.statusCode = status;
1377
+ res.html = function (html, status) {
1378
+ res.statusCode = status !== undefined ? status : (statusCode || 200);
1976
1379
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1977
1380
  res.end(html);
1978
1381
  };
@@ -2067,6 +1470,42 @@ ${cyan} (req, res, next) => {
2067
1470
  };
2068
1471
  }
2069
1472
 
1473
+ _defaultHtmlEngine(filePath, locals, callback) {
1474
+ fs.readFile(filePath, 'utf-8', (err, content) => {
1475
+ if (err) return callback(err);
1476
+
1477
+ let rendered = content;
1478
+
1479
+ Object.keys(locals).forEach(key => {
1480
+ if (locals[key] !== undefined && locals[key] !== null) {
1481
+ const safeRegex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
1482
+ const unsafeRegex = new RegExp(`{{{\\s*${key}\\s*}}}`, 'g');
1483
+
1484
+ if (safeRegex.test(rendered)) {
1485
+ const escaped = this._escapeHtml(String(locals[key]));
1486
+ rendered = rendered.replace(safeRegex, escaped);
1487
+ }
1488
+
1489
+ if (unsafeRegex.test(rendered)) {
1490
+ rendered = rendered.replace(unsafeRegex, String(locals[key]));
1491
+ }
1492
+ }
1493
+ });
1494
+
1495
+ callback(null, rendered);
1496
+ });
1497
+ }
1498
+
1499
+ _escapeHtml(text) {
1500
+ if (typeof text !== 'string') return text;
1501
+ return text
1502
+ .replace(/&/g, '&amp;')
1503
+ .replace(/</g, '&lt;')
1504
+ .replace(/>/g, '&gt;')
1505
+ .replace(/"/g, '&quot;')
1506
+ .replace(/'/g, '&#039;');
1507
+ }
1508
+
2070
1509
  async _runMiddleware(handler, req, res) {
2071
1510
  return new Promise((resolve, reject) => {
2072
1511
  const next = (err) => err ? reject(err) : resolve();
@@ -2145,33 +1584,6 @@ ${cyan} (req, res, next) => {
2145
1584
  console.log('\n' + logLines.join('\n') + '\n');
2146
1585
  }
2147
1586
 
2148
- _logCorsDebug(req, opts) {
2149
- if (!opts.debug) return;
2150
-
2151
- console.log("\n[CORS DEBUG]");
2152
- console.log("Request:", req.method, req.url);
2153
- console.log("Origin:", req.headers.origin || "none");
2154
-
2155
- console.log("Applied CORS Policy:");
2156
- console.log(" - Access-Control-Allow-Origin:", opts.origin);
2157
- console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
2158
- console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
2159
-
2160
- if (opts.credentials) {
2161
- console.log(" - Access-Control-Allow-Credentials: true");
2162
- }
2163
-
2164
- if (opts.exposedHeaders?.length) {
2165
- console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
2166
- }
2167
-
2168
- console.log(" - Max-Age:", opts.maxAge);
2169
-
2170
- if (req.method === "OPTIONS") {
2171
- console.log("Preflight request handled with status 204\n");
2172
- }
2173
- }
2174
-
2175
1587
  listRoutes() {
2176
1588
  const routeEntries = [];
2177
1589
 
@@ -2242,10 +1654,10 @@ ${cyan} (req, res, next) => {
2242
1654
  console.log(` \x1b[36m${r.method.padEnd(7)}\x1b[0m \x1b[33m${pathStr}\x1b[0m \x1b[90m(mw: ${r.mw})\x1b[0m`);
2243
1655
  }
2244
1656
  }
1657
+
2245
1658
  listen() {
2246
1659
  const args = Array.from(arguments);
2247
1660
  const server = createServer(this._handleRequest.bind(this));
2248
-
2249
1661
  server.listen.apply(server, args);
2250
1662
  this.server = server;
2251
1663
  return server;
@@ -2262,13 +1674,10 @@ function Router() {
2262
1674
 
2263
1675
  module.exports = Lieko;
2264
1676
  module.exports.Router = Router;
1677
+
2265
1678
  module.exports.Schema = Schema;
2266
- module.exports.schema = (...args) => new Schema(...args);
1679
+ module.exports.createSchema = (...args) => new Schema(...args);
2267
1680
  module.exports.validators = validators;
2268
1681
  module.exports.validate = validate;
2269
1682
  module.exports.validatePartial = validatePartial;
2270
1683
  module.exports.ValidationError = ValidationError;
2271
- module.exports.static = function (root, options) {
2272
- const app = new LiekoExpress();
2273
- return app.static(root, options);
2274
- };