kempo-server 1.7.3 → 1.7.5
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/CONFIG.md +501 -0
- package/README.md +13 -442
- package/UTILS.md +127 -0
- package/dist/defaultConfig.js +1 -1
- package/dist/moduleCache.js +1 -0
- package/dist/router.js +1 -1
- package/dist/serveFile.js +1 -1
- package/dist/utils/cli.js +1 -0
- package/dist/utils/fs-utils.js +1 -0
- package/package.json +7 -2
- package/scripts/build.js +57 -43
- package/tests/router-wildcard-double-asterisk.node-test.js +66 -0
- package/config-examples/development.config.json +0 -24
- package/config-examples/low-memory.config.json +0 -23
- package/config-examples/no-cache.config.json +0 -13
- package/config-examples/production.config.json +0 -38
- package/example-cache.config.json +0 -45
- package/example.config.json +0 -50
- package/utils/cache-demo.js +0 -145
- package/utils/cache-monitor.js +0 -132
package/README.md
CHANGED
|
@@ -211,435 +211,19 @@ export default async function(request, response) {
|
|
|
211
211
|
|
|
212
212
|
## Configuration
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
Kempo Server can be customized with a simple JSON configuration file to control caching, middleware, security, routing, and more.
|
|
215
215
|
|
|
216
|
-
**
|
|
217
|
-
- When using a relative path for the `--config` flag, the config file must be located within the server root directory
|
|
218
|
-
- When using an absolute path for the `--config` flag, the config file can be located anywhere on the filesystem
|
|
219
|
-
- The server will throw an error if you attempt to use a relative config file path that points outside the root directory
|
|
216
|
+
For detailed configuration options and examples, see **[CONFIG.md](./CONFIG.md)**.
|
|
220
217
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
- [customRoutes](#customroutes)
|
|
226
|
-
- [routeFiles](#routefiles)
|
|
227
|
-
- [noRescanPaths](#norescanpaths)
|
|
228
|
-
- [maxRescanAttempts](#maxrescanattempts)
|
|
229
|
-
- [cache](#cache)
|
|
230
|
-
- [middleware](#middleware)
|
|
231
|
-
|
|
232
|
-
## Cache
|
|
233
|
-
|
|
234
|
-
Kempo Server includes an intelligent module caching system that dramatically improves performance by caching JavaScript route modules in memory. The cache combines multiple strategies:
|
|
235
|
-
|
|
236
|
-
- **LRU (Least Recently Used)** - Evicts oldest modules when cache fills
|
|
237
|
-
- **Time-based expiration** - Modules expire after configurable TTL
|
|
238
|
-
- **Memory monitoring** - Automatically clears cache if memory usage gets too high
|
|
239
|
-
- **File watching** - Instantly invalidates cache when files change (development)
|
|
240
|
-
|
|
241
|
-
### Basic Cache Configuration
|
|
242
|
-
|
|
243
|
-
Enable caching in your `.config.json`:
|
|
244
|
-
|
|
245
|
-
```json
|
|
246
|
-
{
|
|
247
|
-
"cache": {
|
|
248
|
-
"enabled": true,
|
|
249
|
-
"maxSize": 100,
|
|
250
|
-
"maxMemoryMB": 50,
|
|
251
|
-
"ttlMs": 300000,
|
|
252
|
-
"watchFiles": true
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Cache Options
|
|
258
|
-
|
|
259
|
-
- `enabled` (boolean) - Enable/disable caching (default: `true`)
|
|
260
|
-
- `maxSize` (number) - Maximum cached modules (default: `100`)
|
|
261
|
-
- `maxMemoryMB` (number) - Memory limit in MB (default: `50`)
|
|
262
|
-
- `ttlMs` (number) - Cache lifetime in milliseconds (default: `300000` - 5 minutes)
|
|
263
|
-
- `maxHeapUsagePercent` (number) - Clear cache when heap exceeds % (default: `70`)
|
|
264
|
-
- `memoryCheckInterval` (number) - Memory check frequency in ms (default: `30000`)
|
|
265
|
-
- `watchFiles` (boolean) - Auto-invalidate on file changes (default: `true`)
|
|
266
|
-
- `enableMemoryMonitoring` (boolean) - Enable memory monitoring (default: `true`)
|
|
267
|
-
|
|
268
|
-
### Environment-Specific Configurations
|
|
269
|
-
|
|
270
|
-
Use different config files for different environments:
|
|
271
|
-
|
|
272
|
-
**Development (`dev.config.json`):**
|
|
273
|
-
```json
|
|
274
|
-
{
|
|
275
|
-
"cache": {
|
|
276
|
-
"enabled": true,
|
|
277
|
-
"maxSize": 50,
|
|
278
|
-
"ttlMs": 300000,
|
|
279
|
-
"watchFiles": true
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
**Production (`prod.config.json`):**
|
|
285
|
-
```json
|
|
286
|
-
{
|
|
287
|
-
"cache": {
|
|
288
|
-
"enabled": true,
|
|
289
|
-
"maxSize": 1000,
|
|
290
|
-
"maxMemoryMB": 200,
|
|
291
|
-
"ttlMs": 3600000,
|
|
292
|
-
"watchFiles": false
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Run with specific config: `node src/index.js --config prod.config.json`
|
|
298
|
-
|
|
299
|
-
### Cache Monitoring
|
|
300
|
-
|
|
301
|
-
Monitor cache performance at runtime:
|
|
302
|
-
|
|
303
|
-
- **View stats:** `GET /_admin/cache` - Returns detailed cache statistics
|
|
304
|
-
- **Clear cache:** `DELETE /_admin/cache` - Clears entire cache
|
|
305
|
-
|
|
306
|
-
Example response:
|
|
307
|
-
```json
|
|
308
|
-
{
|
|
309
|
-
"cache": {
|
|
310
|
-
"size": 45,
|
|
311
|
-
"maxSize": 100,
|
|
312
|
-
"memoryUsageMB": 12.5,
|
|
313
|
-
"hitRate": 87
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
## Middleware
|
|
319
|
-
|
|
320
|
-
Kempo Server includes a powerful middleware system that allows you to add functionality like authentication, logging, CORS, compression, and more. Middleware runs before your route handlers and can modify requests, responses, or handle requests entirely.
|
|
321
|
-
|
|
322
|
-
### Built-in Middleware
|
|
323
|
-
|
|
324
|
-
#### CORS
|
|
325
|
-
Enable Cross-Origin Resource Sharing for your API:
|
|
326
|
-
|
|
327
|
-
```json
|
|
328
|
-
{
|
|
329
|
-
"middleware": {
|
|
330
|
-
"cors": {
|
|
331
|
-
"enabled": true,
|
|
332
|
-
"origin": "*",
|
|
333
|
-
"methods": ["GET", "POST", "PUT", "DELETE"],
|
|
334
|
-
"headers": ["Content-Type", "Authorization"]
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
#### Compression
|
|
341
|
-
Automatically compress responses with gzip:
|
|
342
|
-
|
|
343
|
-
```json
|
|
344
|
-
{
|
|
345
|
-
"middleware": {
|
|
346
|
-
"compression": {
|
|
347
|
-
"enabled": true,
|
|
348
|
-
"threshold": 1024
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
#### Rate Limiting
|
|
355
|
-
Limit requests per client to prevent abuse:
|
|
356
|
-
|
|
357
|
-
```json
|
|
358
|
-
{
|
|
359
|
-
"middleware": {
|
|
360
|
-
"rateLimit": {
|
|
361
|
-
"enabled": true,
|
|
362
|
-
"maxRequests": 100,
|
|
363
|
-
"windowMs": 60000,
|
|
364
|
-
"message": "Too many requests"
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
#### Security Headers
|
|
371
|
-
Add security headers to all responses:
|
|
372
|
-
|
|
373
|
-
```json
|
|
374
|
-
{
|
|
375
|
-
"middleware": {
|
|
376
|
-
"security": {
|
|
377
|
-
"enabled": true,
|
|
378
|
-
"headers": {
|
|
379
|
-
"X-Content-Type-Options": "nosniff",
|
|
380
|
-
"X-Frame-Options": "DENY",
|
|
381
|
-
"X-XSS-Protection": "1; mode=block"
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
#### Request Logging
|
|
389
|
-
Log requests with configurable detail:
|
|
390
|
-
|
|
391
|
-
```json
|
|
392
|
-
{
|
|
393
|
-
"middleware": {
|
|
394
|
-
"logging": {
|
|
395
|
-
"enabled": true,
|
|
396
|
-
"includeUserAgent": true,
|
|
397
|
-
"includeResponseTime": true
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### Custom Middleware
|
|
404
|
-
|
|
405
|
-
Create your own middleware by writing JavaScript files and referencing them in your config:
|
|
406
|
-
|
|
407
|
-
```json
|
|
408
|
-
{
|
|
409
|
-
"middleware": {
|
|
410
|
-
"custom": ["./middleware/auth.js", "./middleware/analytics.js"]
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
#### Custom Middleware Example
|
|
416
|
-
|
|
417
|
-
```javascript
|
|
418
|
-
// middleware/auth.js
|
|
419
|
-
export default (config) => {
|
|
420
|
-
return async (req, res, next) => {
|
|
421
|
-
const token = req.headers.authorization;
|
|
422
|
-
|
|
423
|
-
if (!token) {
|
|
424
|
-
req.user = null;
|
|
425
|
-
return await next();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
// Verify JWT token (example)
|
|
430
|
-
const user = verifyToken(token);
|
|
431
|
-
req.user = user;
|
|
432
|
-
req.permissions = await getUserPermissions(user.id);
|
|
433
|
-
|
|
434
|
-
// Add helper methods
|
|
435
|
-
req.hasPermission = (permission) => req.permissions.includes(permission);
|
|
436
|
-
|
|
437
|
-
} catch (error) {
|
|
438
|
-
req.user = null;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
await next();
|
|
442
|
-
};
|
|
443
|
-
};
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
#### Using Enhanced Requests in Routes
|
|
447
|
-
|
|
448
|
-
Your route files can now access the enhanced request object:
|
|
449
|
-
|
|
450
|
-
```javascript
|
|
451
|
-
// api/user/profile/GET.js
|
|
452
|
-
export default async (req, res, params) => {
|
|
453
|
-
if (!req.user) {
|
|
454
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (!req.hasPermission('user:read')) {
|
|
458
|
-
return res.status(403).json({ error: 'Insufficient permissions' });
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const profile = await getUserProfile(req.user.id);
|
|
462
|
-
res.json(profile);
|
|
463
|
-
};
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### Middleware Order
|
|
467
|
-
|
|
468
|
-
Middleware executes in this order:
|
|
469
|
-
1. Built-in middleware (cors, compression, rateLimit, security, logging)
|
|
470
|
-
2. Custom middleware (in the order listed in config)
|
|
471
|
-
3. Your route handlers
|
|
472
|
-
|
|
473
|
-
### Route Interception
|
|
474
|
-
|
|
475
|
-
Middleware can intercept and handle routes completely, useful for authentication endpoints:
|
|
476
|
-
|
|
477
|
-
```javascript
|
|
478
|
-
// middleware/auth-routes.js
|
|
479
|
-
export default (config) => {
|
|
480
|
-
return async (req, res, next) => {
|
|
481
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
482
|
-
|
|
483
|
-
// Handle login endpoint
|
|
484
|
-
if (req.method === 'POST' && url.pathname === '/auth/login') {
|
|
485
|
-
const credentials = await req.json();
|
|
486
|
-
const token = await authenticateUser(credentials);
|
|
487
|
-
|
|
488
|
-
if (token) {
|
|
489
|
-
return res.json({ token, success: true });
|
|
490
|
-
} else {
|
|
491
|
-
return res.status(401).json({ error: 'Invalid credentials' });
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
await next();
|
|
496
|
-
};
|
|
497
|
-
};
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### allowedMimes
|
|
501
|
-
|
|
502
|
-
An object mapping file extensions to their MIME types. Files with extensions not in this list will not be served.
|
|
503
|
-
|
|
504
|
-
```json
|
|
505
|
-
{
|
|
506
|
-
"allowedMimes": {
|
|
507
|
-
"html": "text/html",
|
|
508
|
-
"css": "text/css",
|
|
509
|
-
"js": "application/javascript",
|
|
510
|
-
"json": "application/json",
|
|
511
|
-
"png": "image/png",
|
|
512
|
-
"jpg": "image/jpeg"
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### disallowedRegex
|
|
518
|
-
|
|
519
|
-
An array of regular expressions that match paths that should never be served. This provides security by preventing access to sensitive files.
|
|
520
|
-
|
|
521
|
-
```json
|
|
522
|
-
{
|
|
523
|
-
"disallowedRegex": [
|
|
524
|
-
"^/\\..*",
|
|
525
|
-
"\\.env$",
|
|
526
|
-
"\\.config$",
|
|
527
|
-
"password"
|
|
528
|
-
]
|
|
529
|
-
}
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
### routeFiles
|
|
533
|
-
|
|
534
|
-
An array of filenames that should be treated as route handlers and executed as JavaScript modules.
|
|
535
|
-
|
|
536
|
-
```json
|
|
537
|
-
{
|
|
538
|
-
"routeFiles": [
|
|
539
|
-
"GET.js",
|
|
540
|
-
"POST.js",
|
|
541
|
-
"PUT.js",
|
|
542
|
-
"DELETE.js",
|
|
543
|
-
"index.js"
|
|
544
|
-
]
|
|
545
|
-
}
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### noRescanPaths
|
|
549
|
-
|
|
550
|
-
An array of regex patterns for paths that should not trigger a file system rescan. This improves performance for common static assets.
|
|
551
|
-
|
|
552
|
-
```json
|
|
553
|
-
{
|
|
554
|
-
"noRescanPaths": [
|
|
555
|
-
"/favicon\\.ico$",
|
|
556
|
-
"/robots\\.txt$",
|
|
557
|
-
"\\.map$"
|
|
558
|
-
]
|
|
559
|
-
}
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
### customRoutes
|
|
563
|
-
|
|
564
|
-
An object mapping custom route paths to file paths. Useful for aliasing or serving files from outside the document root.
|
|
565
|
-
|
|
566
|
-
**Note:** All file paths in customRoutes are resolved relative to the server root directory (the `--root` path). This allows you to reference files both inside and outside the document root.
|
|
567
|
-
|
|
568
|
-
**Basic Routes:**
|
|
569
|
-
```json
|
|
570
|
-
{
|
|
571
|
-
"customRoutes": {
|
|
572
|
-
"/vendor/bootstrap.css": "../node_modules/bootstrap/dist/css/bootstrap.min.css",
|
|
573
|
-
"/api/status": "./status.js"
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
**Wildcard Routes:**
|
|
579
|
-
Wildcard routes allow you to map entire directory structures using the `*` and `**` wildcards:
|
|
580
|
-
|
|
581
|
-
```json
|
|
582
|
-
{
|
|
583
|
-
"customRoutes": {
|
|
584
|
-
"kempo/*": "../node_modules/kempo/dist/*",
|
|
585
|
-
"assets/*": "../static-files/*",
|
|
586
|
-
"docs/*": "../documentation/*",
|
|
587
|
-
"src/**": "../src/**"
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
With wildcard routes:
|
|
593
|
-
- `kempo/styles.css` would serve `../node_modules/kempo/dist/styles.css`
|
|
594
|
-
- `assets/logo.png` would serve `../static-files/logo.png`
|
|
595
|
-
- `docs/readme.md` would serve `../documentation/readme.md`
|
|
596
|
-
- `src/components/Button.js` would serve `../src/components/Button.js`
|
|
597
|
-
|
|
598
|
-
The `*` wildcard matches any single path segment (anything between `/` characters).
|
|
599
|
-
The `**` wildcard matches any number of path segments, allowing you to map entire directory trees.
|
|
600
|
-
Multiple wildcards can be used in a single route pattern.
|
|
601
|
-
|
|
602
|
-
### maxRescanAttempts
|
|
603
|
-
|
|
604
|
-
The maximum number of times to attempt rescanning the file system when a file is not found. Defaults to 3.
|
|
605
|
-
|
|
606
|
-
```json
|
|
607
|
-
{
|
|
608
|
-
"maxRescanAttempts": 3
|
|
609
|
-
}
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### cache
|
|
613
|
-
|
|
614
|
-
Configure the intelligent module caching system for improved performance:
|
|
218
|
+
Quick start:
|
|
219
|
+
```bash
|
|
220
|
+
# Create a basic config file
|
|
221
|
+
echo '{"cache": {"enabled": true}}' > .config.json
|
|
615
222
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
"cache": {
|
|
619
|
-
"enabled": true,
|
|
620
|
-
"maxSize": 100,
|
|
621
|
-
"maxMemoryMB": 50,
|
|
622
|
-
"ttlMs": 300000,
|
|
623
|
-
"maxHeapUsagePercent": 70,
|
|
624
|
-
"memoryCheckInterval": 30000,
|
|
625
|
-
"watchFiles": true,
|
|
626
|
-
"enableMemoryMonitoring": true
|
|
627
|
-
}
|
|
628
|
-
}
|
|
223
|
+
# Use different configs for different environments
|
|
224
|
+
kempo-server --root public --config dev.config.json
|
|
629
225
|
```
|
|
630
226
|
|
|
631
|
-
Cache options:
|
|
632
|
-
- `enabled` - Enable/disable caching (boolean, default: `true`)
|
|
633
|
-
- `maxSize` - Maximum cached modules (number, default: `100`)
|
|
634
|
-
- `maxMemoryMB` - Memory limit in MB (number, default: `50`)
|
|
635
|
-
- `ttlMs` - Cache lifetime in milliseconds (number, default: `300000`)
|
|
636
|
-
- `maxHeapUsagePercent` - Clear cache when heap exceeds % (number, default: `70`)
|
|
637
|
-
- `memoryCheckInterval` - Memory check frequency in ms (number, default: `30000`)
|
|
638
|
-
- `watchFiles` - Auto-invalidate on file changes (boolean, default: `true`)
|
|
639
|
-
- `enableMemoryMonitoring` - Enable memory monitoring (boolean, default: `true`)
|
|
640
|
-
|
|
641
|
-
Use different config files for different environments rather than nested objects.
|
|
642
|
-
|
|
643
227
|
## Features
|
|
644
228
|
|
|
645
229
|
- **Zero Dependencies** - No external dependencies required
|
|
@@ -756,24 +340,6 @@ Kempo Server supports several command line options to customize its behavior:
|
|
|
756
340
|
kempo-server --root public --port 8080 --host 0.0.0.0 --verbose
|
|
757
341
|
```
|
|
758
342
|
|
|
759
|
-
### Configuration File Examples
|
|
760
|
-
|
|
761
|
-
You can specify different configuration files for different environments:
|
|
762
|
-
|
|
763
|
-
```bash
|
|
764
|
-
# Development
|
|
765
|
-
kempo-server --root public --config dev.config.json
|
|
766
|
-
|
|
767
|
-
# Staging
|
|
768
|
-
kempo-server --root public --config staging.config.json
|
|
769
|
-
|
|
770
|
-
# Production with absolute path
|
|
771
|
-
kempo-server --root public --config /etc/kempo/production.config.json
|
|
772
|
-
|
|
773
|
-
# Mix with other options
|
|
774
|
-
kempo-server --root dist --port 8080 --config production.config.json --scan
|
|
775
|
-
```
|
|
776
|
-
|
|
777
343
|
## Testing
|
|
778
344
|
|
|
779
345
|
This project uses the Kempo Testing Framework. Tests live in the `tests/` folder and follow these naming conventions:
|
|
@@ -796,3 +362,8 @@ npm run tests:gui # Start the GUI test runner
|
|
|
796
362
|
For advanced usage (filters, flags, GUI options), see:
|
|
797
363
|
https://github.com/dustinpoissant/kempo-testing-framework
|
|
798
364
|
|
|
365
|
+
## Documentation
|
|
366
|
+
|
|
367
|
+
- **[CONFIG.md](./CONFIG.md)** - Comprehensive server configuration guide
|
|
368
|
+
- **[UTILS.md](./UTILS.md)** - Utility modules for Node.js projects
|
|
369
|
+
|
package/UTILS.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Utility Modules
|
|
2
|
+
|
|
3
|
+
Kempo Server includes some utility modules that can be used in your own Node.js projects. These utilities are built and exported alongside the main server package.
|
|
4
|
+
|
|
5
|
+
> **Note:** These utilities are included here for convenience but may be moved to their own dedicated package in the future as more Node.js utilities are added.
|
|
6
|
+
|
|
7
|
+
## CLI Utilities
|
|
8
|
+
|
|
9
|
+
The CLI utilities provide simple command-line argument parsing functionality.
|
|
10
|
+
|
|
11
|
+
### Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { getArgs } from 'kempo-server/utils/cli';
|
|
15
|
+
|
|
16
|
+
// Basic usage - get all arguments as key-value pairs
|
|
17
|
+
const args = getArgs();
|
|
18
|
+
console.log(args); // { port: '3000', host: 'localhost', verbose: true }
|
|
19
|
+
|
|
20
|
+
// With mapping - map short flags to full names
|
|
21
|
+
const args = getArgs({
|
|
22
|
+
'p': 'port',
|
|
23
|
+
'h': 'host',
|
|
24
|
+
'v': 'verbose'
|
|
25
|
+
});
|
|
26
|
+
console.log(args); // Maps -p to port, -h to host, -v to verbose
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Example
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node myapp.js --port 8080 -h 127.0.0.1 --verbose
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
import { getArgs } from 'kempo-server/utils/cli';
|
|
37
|
+
|
|
38
|
+
const args = getArgs({
|
|
39
|
+
'h': 'host',
|
|
40
|
+
'p': 'port',
|
|
41
|
+
'v': 'verbose'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Results in:
|
|
45
|
+
// {
|
|
46
|
+
// host: '127.0.0.1',
|
|
47
|
+
// port: '8080',
|
|
48
|
+
// verbose: true
|
|
49
|
+
// }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## File System Utilities
|
|
53
|
+
|
|
54
|
+
The file system utilities provide common file and directory operations with Promise-based APIs.
|
|
55
|
+
|
|
56
|
+
### Usage
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
import { ensureDir, copyDir } from 'kempo-server/utils/fs-utils';
|
|
60
|
+
|
|
61
|
+
// Ensure a directory exists (creates all parent directories if needed)
|
|
62
|
+
await ensureDir('./my/nested/directory');
|
|
63
|
+
|
|
64
|
+
// Copy an entire directory structure recursively
|
|
65
|
+
await copyDir('./source', './destination');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Functions
|
|
69
|
+
|
|
70
|
+
#### `ensureDir(dirPath)`
|
|
71
|
+
|
|
72
|
+
Ensures that a directory exists, creating it and any necessary parent directories if they don't exist. Similar to `mkdir -p`.
|
|
73
|
+
|
|
74
|
+
- **Parameters:**
|
|
75
|
+
- `dirPath` (string) - The directory path to ensure exists
|
|
76
|
+
- **Returns:** Promise that resolves when the directory is confirmed to exist
|
|
77
|
+
|
|
78
|
+
#### `copyDir(srcPath, destPath)`
|
|
79
|
+
|
|
80
|
+
Recursively copies an entire directory structure from source to destination.
|
|
81
|
+
|
|
82
|
+
- **Parameters:**
|
|
83
|
+
- `srcPath` (string) - The source directory to copy from
|
|
84
|
+
- `destPath` (string) - The destination directory to copy to
|
|
85
|
+
- **Returns:** Promise that resolves when the copy operation is complete
|
|
86
|
+
|
|
87
|
+
### Example Use Cases
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import { ensureDir, copyDir } from 'kempo-server/utils/fs-utils';
|
|
91
|
+
|
|
92
|
+
// Build script example
|
|
93
|
+
async function buildProject() {
|
|
94
|
+
// Ensure build directories exist
|
|
95
|
+
await ensureDir('./dist/assets');
|
|
96
|
+
await ensureDir('./dist/components');
|
|
97
|
+
|
|
98
|
+
// Copy static assets
|
|
99
|
+
await copyDir('./src/assets', './dist/assets');
|
|
100
|
+
await copyDir('./src/public', './dist');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Backup script example
|
|
104
|
+
async function backupProject() {
|
|
105
|
+
const timestamp = new Date().toISOString().slice(0, 10);
|
|
106
|
+
const backupPath = `./backups/${timestamp}`;
|
|
107
|
+
|
|
108
|
+
await ensureDir(backupPath);
|
|
109
|
+
await copyDir('./src', `${backupPath}/src`);
|
|
110
|
+
await copyDir('./config', `${backupPath}/config`);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Installation and Build
|
|
115
|
+
|
|
116
|
+
These utilities are automatically built when you install kempo-server. They're available through the package's exports:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"exports": {
|
|
121
|
+
"./utils/cli": "./dist/utils/cli.js",
|
|
122
|
+
"./utils/fs-utils": "./dist/utils/fs-utils.js"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The utilities are ES modules and require Node.js environments that support ES module imports.
|
package/dist/defaultConfig.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default{allowedMimes:{html:"text/html",htm:"text/html",shtml:"text/html",css:"text/css",xml:"text/xml",gif:"image/gif",jpeg:"image/jpeg",jpg:"image/jpeg",js:"application/javascript",mjs:"application/javascript",json:"application/json",webp:"image/webp",png:"image/png",svg:"image/svg+xml",svgz:"image/svg+xml",ico:"image/x-icon",webm:"video/webm",mp4:"video/mp4",m4v:"video/mp4",ogv:"video/ogg",mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",otf:"font/otf",eot:"application/vnd.ms-fontobject",pdf:"application/pdf",txt:"text/plain",webmanifest:"application/manifest+json",md:"text/markdown",csv:"text/csv",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",avif:"image/avif",wasm:"application/wasm"},disallowedRegex:["^/\\..*","\\.config$","\\.env$","\\.git/","\\.htaccess$","\\.htpasswd$","^/node_modules/","^/vendor/","\\.log$","\\.bak$","\\.sql$","\\.ini$","password","config\\.php$","wp-config\\.php$","\\.DS_Store$"],routeFiles:["GET.js","POST.js","PUT.js","DELETE.js","HEAD.js","OPTIONS.js","PATCH.js","CONNECT.js","TRACE.js","index.js"],noRescanPaths:["^\\.well-known/","/favicon\\.ico$","/robots\\.txt$","/sitemap\\.xml$","/apple-touch-icon","/android-chrome-","/browserconfig\\.xml$","/manifest\\.json$","\\.map$","/__webpack_hmr$","/hot-update\\.","/sockjs-node/"],maxRescanAttempts:3,customRoutes:{},middleware:{cors:{enabled:!1,origin:"*",methods:["GET","POST","PUT","DELETE","OPTIONS"],headers:["Content-Type","Authorization"]},compression:{enabled:!1,threshold:1024},rateLimit:{enabled:!1,maxRequests:100,windowMs:6e4,message:"Too many requests"},security:{enabled:!0,headers:{"X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","X-XSS-Protection":"1; mode=block"}},logging:{enabled:!0,includeUserAgent:!1,includeResponseTime:!0},custom:[]}};
|
|
1
|
+
export default{allowedMimes:{html:"text/html",htm:"text/html",shtml:"text/html",css:"text/css",xml:"text/xml",gif:"image/gif",jpeg:"image/jpeg",jpg:"image/jpeg",js:"application/javascript",mjs:"application/javascript",json:"application/json",webp:"image/webp",png:"image/png",svg:"image/svg+xml",svgz:"image/svg+xml",ico:"image/x-icon",webm:"video/webm",mp4:"video/mp4",m4v:"video/mp4",ogv:"video/ogg",mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",otf:"font/otf",eot:"application/vnd.ms-fontobject",pdf:"application/pdf",txt:"text/plain",webmanifest:"application/manifest+json",md:"text/markdown",csv:"text/csv",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",avif:"image/avif",wasm:"application/wasm"},disallowedRegex:["^/\\..*","\\.config$","\\.env$","\\.git/","\\.htaccess$","\\.htpasswd$","^/node_modules/","^/vendor/","\\.log$","\\.bak$","\\.sql$","\\.ini$","password","config\\.php$","wp-config\\.php$","\\.DS_Store$"],routeFiles:["GET.js","POST.js","PUT.js","DELETE.js","HEAD.js","OPTIONS.js","PATCH.js","CONNECT.js","TRACE.js","index.js"],noRescanPaths:["^\\.well-known/","/favicon\\.ico$","/robots\\.txt$","/sitemap\\.xml$","/apple-touch-icon","/android-chrome-","/browserconfig\\.xml$","/manifest\\.json$","\\.map$","/__webpack_hmr$","/hot-update\\.","/sockjs-node/"],maxRescanAttempts:3,customRoutes:{},middleware:{cors:{enabled:!1,origin:"*",methods:["GET","POST","PUT","DELETE","OPTIONS"],headers:["Content-Type","Authorization"]},compression:{enabled:!1,threshold:1024},rateLimit:{enabled:!1,maxRequests:100,windowMs:6e4,message:"Too many requests"},security:{enabled:!0,headers:{"X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","X-XSS-Protection":"1; mode=block"}},logging:{enabled:!0,includeUserAgent:!1,includeResponseTime:!0},custom:[]},cache:{enabled:!1,maxSize:100,maxMemoryMB:50,ttlMs:3e5,maxHeapUsagePercent:70,memoryCheckInterval:3e4,watchFiles:!0,enableMemoryMonitoring:!0}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{watch}from"fs";import path from"path";export default class ModuleCache{constructor(config={}){this.cache=new Map,this.watchers=new Map,this.maxSize=config.maxSize||100,this.maxMemoryMB=config.maxMemoryMB||50,this.ttlMs=config.ttlMs||3e5,this.maxHeapUsagePercent=config.maxHeapUsagePercent||70,this.memoryCheckInterval=config.memoryCheckInterval||3e4,this.watchFiles=!1!==config.watchFiles,this.currentMemoryMB=0,this.stats={hits:0,misses:0,evictions:0,fileChanges:0},!1!==config.enableMemoryMonitoring&&this.startMemoryMonitoring()}startMemoryMonitoring(){this.memoryTimer=setInterval(()=>{const usage=process.memoryUsage(),heapPercent=usage.heapUsed/usage.heapTotal*100;if(heapPercent>this.maxHeapUsagePercent){const clearedCount=this.clearExpiredEntries();for(this.stats.evictions+=clearedCount;this.cache.size>0&&heapPercent>this.maxHeapUsagePercent;)this.evictOldest()}},this.memoryCheckInterval)}destroy(){this.memoryTimer&&clearInterval(this.memoryTimer);for(const watcher of this.watchers.values())watcher.close();this.watchers.clear(),this.cache.clear()}get(filePath,stats){const entry=this.cache.get(filePath);if(!entry)return this.stats.misses++,null;return Date.now()-entry.timestamp>this.ttlMs||entry.mtime<stats.mtime?(this.delete(filePath),this.stats.misses++,null):(this.cache.delete(filePath),this.cache.set(filePath,entry),this.stats.hits++,entry.module)}set(filePath,module,stats,estimatedSizeKB=1){const sizeInMB=estimatedSizeKB/1024;for(;this.cache.size>=this.maxSize||this.currentMemoryMB+sizeInMB>this.maxMemoryMB;)this.evictOldest();const entry={module:module,mtime:stats.mtime,timestamp:Date.now(),sizeKB:estimatedSizeKB,filePath:filePath};this.cache.set(filePath,entry),this.currentMemoryMB+=sizeInMB,this.watchFiles&&this.setupFileWatcher(filePath)}delete(filePath){const entry=this.cache.get(filePath);if(entry){this.cache.delete(filePath),this.currentMemoryMB-=entry.sizeKB/1024;const watcher=this.watchers.get(filePath);return watcher&&(watcher.close(),this.watchers.delete(filePath)),!0}return!1}clear(){const size=this.cache.size;this.cache.clear(),this.currentMemoryMB=0;for(const watcher of this.watchers.values())watcher.close();this.watchers.clear(),this.stats.evictions+=size}evictOldest(){if(0===this.cache.size)return;const[oldestKey,oldestEntry]=this.cache.entries().next().value;this.delete(oldestKey),this.stats.evictions++}clearExpiredEntries(){const now=Date.now();let clearedCount=0;for(const[filePath,entry]of this.cache.entries())now-entry.timestamp>this.ttlMs&&(this.delete(filePath),clearedCount++);return clearedCount}setupFileWatcher(filePath){if(!this.watchers.has(filePath))try{const watcher=watch(filePath,eventType=>{"change"===eventType&&(this.delete(filePath),this.stats.fileChanges++)});watcher.on("error",error=>{this.delete(filePath)}),this.watchers.set(filePath,watcher)}catch(error){console.warn(`Could not watch file ${filePath}: ${error.message}`)}}getStats(){const memoryUsage=process.memoryUsage();return{cache:{size:this.cache.size,maxSize:this.maxSize,memoryUsageMB:Math.round(100*this.currentMemoryMB)/100,maxMemoryMB:this.maxMemoryMB,watchersActive:this.watchers.size},stats:{...this.stats},memory:{heapUsedMB:Math.round(memoryUsage.heapUsed/1024/1024*100)/100,heapTotalMB:Math.round(memoryUsage.heapTotal/1024/1024*100)/100,heapUsagePercent:Math.round(memoryUsage.heapUsed/memoryUsage.heapTotal*100),rssMB:Math.round(memoryUsage.rss/1024/1024*100)/100},config:{ttlMs:this.ttlMs,maxHeapUsagePercent:this.maxHeapUsagePercent,watchFiles:this.watchFiles}}}getHitRate(){const total=this.stats.hits+this.stats.misses;return 0===total?0:Math.round(this.stats.hits/total*100)}logStats(log){const stats=this.getStats();log(`Cache Stats: ${stats.cache.size}/${stats.cache.maxSize} entries, ${stats.cache.memoryUsageMB}/${stats.cache.maxMemoryMB}MB, ${this.getHitRate()}% hit rate`,2)}getCachedFiles(){return Array.from(this.cache.keys()).map(filePath=>({path:filePath,relativePath:path.relative(process.cwd(),filePath),age:Date.now()-this.cache.get(filePath).timestamp,sizeKB:this.cache.get(filePath).sizeKB}))}}
|
package/dist/router.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import path from"path";import{readFile}from"fs/promises";import{pathToFileURL}from"url";import defaultConfig from"./defaultConfig.js";import getFiles from"./getFiles.js";import findFile from"./findFile.js";import serveFile from"./serveFile.js";import MiddlewareRunner from"./middlewareRunner.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";export default async(flags,log)=>{log("Initializing router",2);const rootPath=path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,2);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);log(`Loading config from: ${configPath}`,2);const configContent=await readFile(configPath,"utf8"),userConfig=JSON.parse(configContent);config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes},middleware:{...defaultConfig.middleware,...userConfig.middleware},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes}},log("User config loaded and merged with defaults",2)}catch(e){log("Using default config (no config file found)",2)}const dis=new Set(config.disallowedRegex);dis.add("^/\\..*"),dis.add("\\.config$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,2);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,1);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",2)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",2)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",2)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",2)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",2)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,2);for(const middlewarePath of config.middleware.custom)try{const resolvedPath=path.resolve(middlewarePath),customMiddleware=(await import(pathToFileURL(resolvedPath))).default;"function"==typeof customMiddleware?(middlewareRunner.use(customMiddleware(config.middleware)),log(`Custom middleware loaded: ${middlewarePath}`,2)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}const customRoutes=new Map,wildcardRoutes=new Map;if(config.customRoutes&&Object.keys(config.customRoutes).length>0){log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,2);for(const[urlPath,filePath]of Object.entries(config.customRoutes))
|
|
1
|
+
import path from"path";import{readFile}from"fs/promises";import{pathToFileURL}from"url";import defaultConfig from"./defaultConfig.js";import getFiles from"./getFiles.js";import findFile from"./findFile.js";import serveFile from"./serveFile.js";import MiddlewareRunner from"./middlewareRunner.js";import ModuleCache from"./moduleCache.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";export default async(flags,log)=>{log("Initializing router",2);const rootPath=path.isAbsolute(flags.root)?flags.root:path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,2);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);if(log(`Config file name: ${configFileName}`,2),log(`Config path: ${configPath}`,2),path.isAbsolute(configFileName))log("Config file name is absolute, skipping validation",2);else{const relativeConfigPath=path.relative(rootPath,configPath);if(log(`Relative config path: ${relativeConfigPath}`,2),log(`Starts with '..': ${relativeConfigPath.startsWith("..")}`,2),relativeConfigPath.startsWith("..")||path.isAbsolute(relativeConfigPath))throw log("Validation failed - throwing error",2),new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);log("Validation passed",2)}log(`Loading config from: ${configPath}`,2);const configContent=await readFile(configPath,"utf8"),userConfig=JSON.parse(configContent);config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes},middleware:{...defaultConfig.middleware,...userConfig.middleware},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes},cache:{...defaultConfig.cache,...userConfig.cache}},log("User config loaded and merged with defaults",2)}catch(e){if(e.message.includes("Config file must be within the server root directory"))throw e;log("Using default config (no config file found)",2)}const dis=new Set(config.disallowedRegex);dis.add("^/\\..*"),dis.add("\\.config$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,2);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,1);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",2)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",2)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",2)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",2)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",2)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,2);for(const middlewarePath of config.middleware.custom)try{const resolvedPath=path.resolve(middlewarePath),customMiddleware=(await import(pathToFileURL(resolvedPath))).default;"function"==typeof customMiddleware?(middlewareRunner.use(customMiddleware(config.middleware)),log(`Custom middleware loaded: ${middlewarePath}`,2)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}let moduleCache=null;config.cache?.enabled&&(moduleCache=new ModuleCache(config.cache),log(`Module cache initialized: ${config.cache.maxSize} max modules, ${config.cache.maxMemoryMB}MB limit, ${config.cache.ttlMs}ms TTL`,2));const customRoutes=new Map,wildcardRoutes=new Map;if(config.customRoutes&&Object.keys(config.customRoutes).length>0){log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,2);for(const[urlPath,filePath]of Object.entries(config.customRoutes))if(urlPath.includes("*")){const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);wildcardRoutes.set(urlPath,resolvedPath),log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`,2)}else{const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,2)}}const matchWildcardRoute=(requestPath,pattern)=>{const regexPattern=pattern.replace(/\*\*/g,"(.+)").replace(/\*/g,"([^/]+)");return new RegExp(`^${regexPattern}$`).exec(requestPath)},rescanAttempts=new Map,dynamicNoRescanPaths=new Set,shouldSkipRescan=requestPath=>config.noRescanPaths.some(pattern=>new RegExp(pattern).test(requestPath))?(log(`Skipping rescan for configured pattern: ${requestPath}`,3),!0):!!dynamicNoRescanPaths.has(requestPath)&&(log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`,3),!0),handler=async(req,res)=>{await middlewareRunner.run(req,res,async()=>{const requestPath=req.url.split("?")[0];log(`${req.method} ${requestPath}`,0),log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(", ")}`,1);const normalizePath=p=>{let np=decodeURIComponent(p);return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np},normalizedRequestPath=normalizePath(requestPath);log(`Normalized requestPath: ${normalizedRequestPath}`,1);let matchedKey=null;for(const key of customRoutes.keys())if(normalizePath(key)===normalizedRequestPath){matchedKey=key;break}if(matchedKey){const customFilePath=customRoutes.get(matchedKey);log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`,2);try{const{stat:stat}=await import("fs/promises");try{await stat(customFilePath),log(`Custom route file exists: ${customFilePath}`,2)}catch(e){return log(`Custom route file does NOT exist: ${customFilePath}`,0),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}const fileContent=await readFile(customFilePath),fileExtension=path.extname(customFilePath).toLowerCase().slice(1),mimeType=config.allowedMimes[fileExtension]||"application/octet-stream";return log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2),res.writeHead(200,{"Content-Type":mimeType}),void res.end(fileContent)}catch(error){return log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const wildcardMatch=(requestPath=>{for(const[pattern,filePath]of wildcardRoutes){const matches=matchWildcardRoute(requestPath,pattern);if(matches)return{filePath:filePath,matches:matches}}return null})(requestPath);if(wildcardMatch){const resolvedFilePath=((filePath,matches)=>{let resolvedPath=filePath,matchIndex=1;for(;resolvedPath.includes("**")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("**",matches[matchIndex]),matchIndex++;for(;resolvedPath.includes("*")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("*",matches[matchIndex]),matchIndex++;return path.isAbsolute(resolvedPath)?resolvedPath:path.resolve(rootPath,resolvedPath)})(wildcardMatch.filePath,wildcardMatch.matches);log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`,2);try{const fileContent=await readFile(resolvedFilePath),fileExtension=path.extname(resolvedFilePath).toLowerCase().slice(1),mimeType=config.allowedMimes[fileExtension]||"application/octet-stream";return log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`,2),res.writeHead(200,{"Content-Type":mimeType}),void res.end(fileContent)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(served||!flags.scan||shouldSkipRescan(requestPath))served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"));else{(requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>=config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,2)})(requestPath),log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)||(log(`404 - File not found after rescan: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"))}})};return handler.moduleCache=moduleCache,handler.getStats=()=>moduleCache?.getStats()||null,handler.logCacheStats=()=>moduleCache?.logStats(log),handler.clearCache=()=>moduleCache?.clear(),handler};
|
package/dist/serveFile.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import path from"path";import{readFile}from"fs/promises";import{pathToFileURL}from"url";import findFile from"./findFile.js";import createRequestWrapper from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";export default async(files,rootPath,requestPath,method,config,req,res,log)=>{log(`Attempting to serve: ${requestPath}`,3);const[file,params]=await findFile(files,rootPath,requestPath,method,log);if(!file)return log(`No file found for: ${requestPath}`,3),!1;const fileName=path.basename(file);if(log(`Found file: ${file}`,2),config.routeFiles.includes(fileName)){log(`Executing route file: ${fileName}`,2);try{const fileUrl=pathToFileURL(file).href
|
|
1
|
+
import path from"path";import{readFile,stat}from"fs/promises";import{pathToFileURL}from"url";import findFile from"./findFile.js";import createRequestWrapper from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";export default async(files,rootPath,requestPath,method,config,req,res,log,moduleCache=null)=>{log(`Attempting to serve: ${requestPath}`,3);const[file,params]=await findFile(files,rootPath,requestPath,method,log);if(!file)return log(`No file found for: ${requestPath}`,3),!1;const fileName=path.basename(file);if(log(`Found file: ${file}`,2),config.routeFiles.includes(fileName)){log(`Executing route file: ${fileName}`,2);try{let module;if(moduleCache&&config.cache?.enabled){const fileStats=await stat(file);if(module=moduleCache.get(file,fileStats),module)log(`Using cached module: ${fileName}`,3);else{const fileUrl=pathToFileURL(file).href+`?t=${Date.now()}`;log(`Loading module from: ${fileUrl}`,3),module=await import(fileUrl);const estimatedSizeKB=fileStats.size/1024;moduleCache.set(file,module,fileStats,estimatedSizeKB),log(`Cached module: ${fileName} (${estimatedSizeKB.toFixed(1)}KB)`,3)}}else{const fileUrl=pathToFileURL(file).href+`?t=${Date.now()}`;log(`Loading module from: ${fileUrl}`,3),module=await import(fileUrl)}if("function"==typeof module.default){log(`Executing route function with params: ${JSON.stringify(params)}`,3);const enhancedRequest=createRequestWrapper(req,params),enhancedResponse=createResponseWrapper(res);return moduleCache&&(enhancedRequest._kempoCache=moduleCache),await module.default(enhancedRequest,enhancedResponse),log(`Route executed successfully: ${fileName}`,2),!0}return log(`Route file does not export a function: ${fileName}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),res.end("Route file does not export a function"),!0}catch(error){return log(`Error loading route file ${fileName}: ${error.message}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),res.end("Internal Server Error"),!0}}else{log(`Serving static file: ${fileName}`,2);try{const fileContent=await readFile(file),fileExtension=path.extname(file).toLowerCase().slice(1),mimeType=config.allowedMimes[fileExtension]||"application/octet-stream";return log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`,2),res.writeHead(200,{"Content-Type":mimeType}),res.end(fileContent),!0}catch(error){return log(`Error reading file ${file}: ${error.message}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),res.end("Internal Server Error"),!0}}};
|