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/README.md CHANGED
@@ -211,435 +211,19 @@ export default async function(request, response) {
211
211
 
212
212
  ## Configuration
213
213
 
214
- To configure the server, create a configuration file (by default `.config.json`) within the root directory of your server (`public` in the start example [above](#getting-started)). You can specify a different configuration file using the `--config` flag.
214
+ Kempo Server can be customized with a simple JSON configuration file to control caching, middleware, security, routing, and more.
215
215
 
216
- **Important:**
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
- This json file can have any of the following properties, any property not defined will use the "Default Config".
222
-
223
- - [allowedMimes](#allowedmimes)
224
- - [disallowedRegex](#disallowedregex)
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
- ```json
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.
@@ -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))try{if(urlPath.includes("*"))wildcardRoutes.set(urlPath,filePath),log(`Wildcard route mapped: ${urlPath} -> ${filePath}`,2);else{const resolvedPath=path.resolve(filePath),{stat:stat}=await import("fs/promises");await stat(resolvedPath),customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,2)}}catch(error){log(`Custom route error for ${urlPath} -> ${filePath}: ${error.message}`,1)}}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);return async(req,res)=>{await middlewareRunner.run(req,res,async()=>{const requestPath=req.url.split("?")[0];if(log(`${req.method} ${requestPath}`,0),customRoutes.has(requestPath)){const customFilePath=customRoutes.get(requestPath);log(`Serving custom route: ${requestPath} -> ${customFilePath}`,2);try{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 ${requestPath}: ${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;for(let i=1;i<matches.length;i++)resolvedPath=resolvedPath.replace("*",matches[i]);return path.resolve(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);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)||(log(`404 - File not found after rescan: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"))}})}};
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;log(`Loading module from: ${fileUrl}`,3);const 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 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}}};
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}}};