koa-classic-server 2.1.4 โ†’ 2.4.0

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
@@ -4,37 +4,31 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/koa-classic-server.svg)](https://www.npmjs.com/package/koa-classic-server)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Tests](https://img.shields.io/badge/tests-153%20passing-brightgreen.svg)]()
7
+ [![Tests](https://img.shields.io/badge/tests-214%20passing-brightgreen.svg)]()
8
8
 
9
9
  ---
10
10
 
11
- ## ๐ŸŽ‰ Version 2.1.3 - Configuration Update
11
+ ## ๐ŸŽ‰ Version 2.X - Production-Ready Release
12
12
 
13
- Version 2.1.3 updates the default caching behavior for better development experience while maintaining production-ready performance.
13
+ The 2.X series brings major performance improvements, enhanced security, and powerful new features while maintaining full backward compatibility.
14
14
 
15
- ### What's New in 2.1.3
16
-
17
- โœ… **Development-Friendly Defaults** - `enableCaching` now defaults to `false` for easier development
18
- โœ… **Production Guidance** - Clear documentation on enabling caching for production environments
19
- โœ… **Enhanced Documentation** - Comprehensive notes on caching configuration and recommendations
20
-
21
- ### What's New in 2.1.2
15
+ ### Key Features in Version 2.X
22
16
 
17
+ โœ… **URL Rewriting Support** - Compatible with i18n and routing middleware via `useOriginalUrl` option
18
+ โœ… **Improved Caching Controls** - Clear `browserCacheEnabled` and `browserCacheMaxAge` options
19
+ โœ… **Development-Friendly Defaults** - Caching disabled by default for easier development
20
+ โœ… **Production Optimized** - Enable caching in production for 80-95% bandwidth reduction
23
21
  โœ… **Sortable Directory Columns** - Click Name/Type/Size to sort (Apache2-like)
24
- โœ… **Navigation Bug Fixed** - Directory navigation now works correctly after sorting
25
22
  โœ… **File Size Display** - Human-readable file sizes (B, KB, MB, GB, TB)
26
- โœ… **HTTP Caching** - 80-95% bandwidth reduction with ETag and Last-Modified
23
+ โœ… **HTTP Caching** - ETag and Last-Modified headers with 304 responses
27
24
  โœ… **Async/Await** - Non-blocking I/O for high performance
28
- โœ… **153 Tests Passing** - Comprehensive test coverage
29
- โœ… **Flow Documentation** - Complete execution flow diagrams
30
- โœ… **Code Review** - Standardized operators and best practices
31
-
32
- ### What's New in 2.0
33
-
34
- โœ… **Performance Optimizations** - 50-70% faster directory listings
25
+ โœ… **Performance Optimized** - 50-70% faster directory listings
35
26
  โœ… **Enhanced Index Option** - Array format with RegExp support
36
- โœ… **Template Engine Guide** - Complete documentation with examples
37
- โœ… **Security Hardened** - Path traversal, XSS, race condition fixes
27
+ โœ… **Template Engine Support** - EJS, Pug, Handlebars, Nunjucks, and more
28
+ โœ… **Enterprise Security** - Path traversal, XSS, race condition protection
29
+ โœ… **Symlink Support** - Full symbolic link support (NixOS, Docker, npm link, Capistrano)
30
+ โœ… **Comprehensive Testing** - 214 tests passing with extensive coverage
31
+ โœ… **Complete Documentation** - Detailed guides and examples
38
32
 
39
33
  [See full changelog โ†’](./docs/CHANGELOG.md)
40
34
 
@@ -55,7 +49,8 @@ Version 2.1.3 updates the default caching behavior for better development experi
55
49
  - ๐Ÿ”’ **Enterprise Security** - Path traversal, XSS, race condition protection
56
50
  - โš™๏ธ **Highly Configurable** - URL prefixes, reserved paths, index files
57
51
  - ๐Ÿš€ **High Performance** - Async/await, non-blocking I/O, optimized algorithms
58
- - ๐Ÿงช **Well-Tested** - 153 passing tests with comprehensive coverage
52
+ - ๐Ÿ”— **Symlink Support** - Transparent symlink resolution with directory listing indicators
53
+ - ๐Ÿงช **Well-Tested** - 214 passing tests with comprehensive coverage
59
54
  - ๐Ÿ“ฆ **Dual Module Support** - CommonJS and ES Modules
60
55
 
61
56
  ---
@@ -101,8 +96,8 @@ app.use(koaClassicServer(__dirname + '/public', {
101
96
  showDirContents: true,
102
97
  index: ['index.html', 'index.htm'],
103
98
  urlPrefix: '/static',
104
- cacheMaxAge: 3600,
105
- enableCaching: true
99
+ browserCacheMaxAge: 3600,
100
+ browserCacheEnabled: true
106
101
  }));
107
102
 
108
103
  app.listen(3000);
@@ -229,14 +224,14 @@ Enable aggressive caching for static files:
229
224
 
230
225
  ```javascript
231
226
  app.use(koaClassicServer(__dirname + '/public', {
232
- enableCaching: true, // Enable ETag and Last-Modified
233
- cacheMaxAge: 86400, // Cache for 24 hours (in seconds)
227
+ browserCacheEnabled: true, // Enable ETag and Last-Modified
228
+ browserCacheMaxAge: 86400, // Cache for 24 hours (in seconds)
234
229
  }));
235
230
  ```
236
231
 
237
232
  **โš ๏ธ Important: Production Recommendation**
238
233
 
239
- The default value for `enableCaching` is `false` to facilitate development (where you want changes to be immediately visible). **For production environments, it is strongly recommended to set `enableCaching: true`** to benefit from:
234
+ The default value for `browserCacheEnabled` is `false` to facilitate development (where you want changes to be immediately visible). **For production environments, it is strongly recommended to set `browserCacheEnabled: true`** to benefit from:
240
235
 
241
236
  - 80-95% bandwidth reduction
242
237
  - 304 Not Modified responses for unchanged files
@@ -281,8 +276,8 @@ app.use(koaClassicServer(path.join(__dirname, 'public'), {
281
276
  index: ['index.html', 'index.htm'],
282
277
  urlPrefix: '/assets',
283
278
  urlsReserved: ['/admin', '/api', '/.git'],
284
- enableCaching: true,
285
- cacheMaxAge: 86400, // 24 hours
279
+ browserCacheEnabled: true,
280
+ browserCacheMaxAge: 86400, // 24 hours
286
281
  }));
287
282
 
288
283
  // Serve dynamic templates
@@ -366,10 +361,18 @@ Creates a Koa middleware for serving static files.
366
361
  ext: ['ejs', 'pug', 'hbs']
367
362
  },
368
363
 
369
- // HTTP caching configuration
364
+ // Browser HTTP caching configuration
370
365
  // NOTE: Default is false for development. Set to true in production for better performance!
371
- enableCaching: false, // Enable ETag & Last-Modified (default: false)
372
- cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
366
+ browserCacheEnabled: false, // Enable ETag & Last-Modified (default: false)
367
+ browserCacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
368
+
369
+ // URL resolution
370
+ useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
371
+ // Set false for URL rewriting middleware (i18n, routing)
372
+
373
+ // DEPRECATED (use new names above):
374
+ // enableCaching: use browserCacheEnabled instead
375
+ // cacheMaxAge: use browserCacheMaxAge instead
373
376
  }
374
377
  ```
375
378
 
@@ -384,8 +387,52 @@ Creates a Koa middleware for serving static files.
384
387
  | `urlsReserved` | Array | `[]` | Reserved directory paths (first-level only) |
385
388
  | `template.render` | Function | `undefined` | Template rendering function |
386
389
  | `template.ext` | Array | `[]` | Extensions for template rendering |
387
- | `enableCaching` | Boolean | `false` | Enable HTTP caching headers (recommended: `true` in production) |
388
- | `cacheMaxAge` | Number | `3600` | Cache duration in seconds |
390
+ | `browserCacheEnabled` | Boolean | `false` | Enable browser HTTP caching headers (recommended: `true` in production) |
391
+ | `browserCacheMaxAge` | Number | `3600` | Browser cache duration in seconds |
392
+ | `useOriginalUrl` | Boolean | `true` | Use `ctx.originalUrl` (default) or `ctx.url` for URL resolution |
393
+ | ~~`enableCaching`~~ | Boolean | `false` | **DEPRECATED**: Use `browserCacheEnabled` instead |
394
+ | ~~`cacheMaxAge`~~ | Number | `3600` | **DEPRECATED**: Use `browserCacheMaxAge` instead |
395
+
396
+ #### useOriginalUrl (Boolean, default: true)
397
+
398
+ Controls which URL property is used for file resolution:
399
+ - **`true` (default)**: Uses `ctx.originalUrl` (immutable, reflects the original request)
400
+ - **`false`**: Uses `ctx.url` (mutable, can be modified by middleware)
401
+
402
+ **When to use `false`:**
403
+
404
+ Set `useOriginalUrl: false` when using URL rewriting middleware such as i18n routers or path rewriters that modify `ctx.url`. This allows koa-classic-server to serve files based on the rewritten URL instead of the original request URL.
405
+
406
+ **Example with i18n middleware:**
407
+
408
+ ```javascript
409
+ const Koa = require('koa');
410
+ const koaClassicServer = require('koa-classic-server');
411
+
412
+ const app = new Koa();
413
+
414
+ // i18n middleware that rewrites URLs
415
+ app.use(async (ctx, next) => {
416
+ if (ctx.path.match(/^\/it\//)) {
417
+ ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html โ†’ /page.html
418
+ }
419
+ await next();
420
+ });
421
+
422
+ // Serve files using rewritten URL
423
+ app.use(koaClassicServer(__dirname + '/www', {
424
+ useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
425
+ }));
426
+
427
+ app.listen(3000);
428
+ ```
429
+
430
+ **How it works:**
431
+ - Request: `GET /it/page.html`
432
+ - `ctx.originalUrl`: `/it/page.html` (unchanged)
433
+ - `ctx.url`: `/page.html` (rewritten by middleware)
434
+ - With `useOriginalUrl: false`: Server looks for `/www/page.html` โœ…
435
+ - With `useOriginalUrl: true` (default): Server looks for `/www/it/page.html` โŒ 404
389
436
 
390
437
  ---
391
438
 
@@ -417,6 +464,33 @@ Human-readable format:
417
464
  - **Click file name** - Download/view file
418
465
  - **Parent Directory** - Go up one level
419
466
 
467
+ ### Symlink Support
468
+
469
+ The middleware fully supports symbolic links, which is essential for environments where served files are symlinks rather than regular files:
470
+
471
+ - **NixOS buildFHSEnv** - Files in www/ appear as symlinks to the Nix store
472
+ - **Docker bind mounts** - Mounted files may appear as symlinks
473
+ - **npm link** - Linked packages are symlinks
474
+ - **Capistrano-style deploys** - The `current` directory is a symlink to the active release
475
+
476
+ **How it works:**
477
+
478
+ Symlinks are followed transparently via `fs.promises.stat()`, but only when `dirent.isSymbolicLink()` is true. Regular files incur zero additional overhead.
479
+
480
+ **Directory listing indicators:**
481
+
482
+ | Entry type | Indicator | Clickable | Type shown |
483
+ |------------|-----------|-----------|------------|
484
+ | Symlink to file | `( Symlink )` | Yes | Target MIME type |
485
+ | Symlink to directory | `( Symlink )` | Yes | `DIR` |
486
+ | Broken/circular symlink | `( Broken Symlink )` | No | `unknown` |
487
+ | Regular file/directory | none | Yes | Real type |
488
+
489
+ **Edge cases handled:**
490
+ - Broken symlinks (missing target) return 404 on direct access
491
+ - Circular symlinks (A โ†’ B โ†’ A) are treated as broken, no infinite loops
492
+ - Symlinks to directories are fully navigable
493
+
420
494
  ---
421
495
 
422
496
  ## Security
@@ -511,10 +585,11 @@ npm run test:performance
511
585
  ```
512
586
 
513
587
  **Test Coverage:**
514
- - โœ… 153 tests passing
588
+ - โœ… 214 tests passing
515
589
  - โœ… Security tests (path traversal, XSS, race conditions)
516
590
  - โœ… EJS template integration tests
517
591
  - โœ… Index option tests (strings, arrays, RegExp)
592
+ - โœ… Symlink tests (file, directory, broken, circular, indicators)
518
593
  - โœ… Performance benchmarks
519
594
  - โœ… Directory sorting tests
520
595
 
@@ -0,0 +1,217 @@
1
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // TEST FOR DEPRECATED OPTION NAMES (enableCaching, cacheMaxAge)
4
+ // This test verifies backward compatibility and deprecation warnings
5
+ //
6
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7
+
8
+ const supertest = require('supertest');
9
+ const koaClassicServer = require('../index.cjs');
10
+ const Koa = require('koa');
11
+ const path = require('path');
12
+
13
+ const rootDir = path.join(__dirname, 'publicWwwTest');
14
+
15
+ describe('Deprecated option names (backward compatibility)', () => {
16
+
17
+ describe('Using deprecated enableCaching option', () => {
18
+ let app;
19
+ let server;
20
+ let consoleWarnSpy;
21
+
22
+ beforeAll(() => {
23
+ // Spy on console.warn to capture deprecation warnings
24
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
25
+
26
+ app = new Koa();
27
+
28
+ // Use deprecated option name
29
+ app.use(koaClassicServer(rootDir, {
30
+ enableCaching: true // DEPRECATED: should use browserCacheEnabled
31
+ }));
32
+
33
+ server = app.listen();
34
+ });
35
+
36
+ afterAll(() => {
37
+ server.close();
38
+ consoleWarnSpy.mockRestore();
39
+ });
40
+
41
+ test('should display deprecation warning for enableCaching', () => {
42
+ expect(consoleWarnSpy).toHaveBeenCalled();
43
+ const warningMessage = consoleWarnSpy.mock.calls[0][1];
44
+ expect(warningMessage).toContain('DEPRECATION WARNING');
45
+ expect(warningMessage).toContain('enableCaching');
46
+ expect(warningMessage).toContain('browserCacheEnabled');
47
+ });
48
+
49
+ test('should still work with deprecated option', async () => {
50
+ const response = await supertest(server).get('/test-page.html');
51
+
52
+ // Should return the file
53
+ expect(response.status).toBe(200);
54
+
55
+ // Should have caching headers (because enableCaching: true was set)
56
+ expect(response.headers['etag']).toBeDefined();
57
+ expect(response.headers['last-modified']).toBeDefined();
58
+ expect(response.headers['cache-control']).toContain('public');
59
+ });
60
+ });
61
+
62
+ describe('Using deprecated cacheMaxAge option', () => {
63
+ let app;
64
+ let server;
65
+ let consoleWarnSpy;
66
+
67
+ beforeAll(() => {
68
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
69
+
70
+ app = new Koa();
71
+
72
+ // Use deprecated option name
73
+ app.use(koaClassicServer(rootDir, {
74
+ enableCaching: true, // Also deprecated
75
+ cacheMaxAge: 7200 // DEPRECATED: should use browserCacheMaxAge
76
+ }));
77
+
78
+ server = app.listen();
79
+ });
80
+
81
+ afterAll(() => {
82
+ server.close();
83
+ consoleWarnSpy.mockRestore();
84
+ });
85
+
86
+ test('should display deprecation warning for cacheMaxAge', () => {
87
+ const warningCalls = consoleWarnSpy.mock.calls;
88
+ const cacheMaxAgeWarning = warningCalls.find(call =>
89
+ call[1] && call[1].includes('cacheMaxAge')
90
+ );
91
+
92
+ expect(cacheMaxAgeWarning).toBeDefined();
93
+ expect(cacheMaxAgeWarning[1]).toContain('DEPRECATION WARNING');
94
+ expect(cacheMaxAgeWarning[1]).toContain('browserCacheMaxAge');
95
+ });
96
+
97
+ test('should use the deprecated cacheMaxAge value', async () => {
98
+ const response = await supertest(server).get('/test-page.html');
99
+
100
+ expect(response.status).toBe(200);
101
+ expect(response.headers['cache-control']).toContain('max-age=7200');
102
+ });
103
+ });
104
+
105
+ describe('Using new option names (no warnings)', () => {
106
+ let app;
107
+ let server;
108
+ let consoleWarnSpy;
109
+
110
+ beforeAll(() => {
111
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
112
+
113
+ app = new Koa();
114
+
115
+ // Use NEW option names
116
+ app.use(koaClassicServer(rootDir, {
117
+ browserCacheEnabled: true,
118
+ browserCacheMaxAge: 3600
119
+ }));
120
+
121
+ server = app.listen();
122
+ });
123
+
124
+ afterAll(() => {
125
+ server.close();
126
+ consoleWarnSpy.mockRestore();
127
+ });
128
+
129
+ test('should NOT display deprecation warnings', () => {
130
+ // Filter out warnings from other tests (like index option deprecation)
131
+ const cachingWarnings = consoleWarnSpy.mock.calls.filter(call =>
132
+ call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
133
+ );
134
+
135
+ expect(cachingWarnings.length).toBe(0);
136
+ });
137
+
138
+ test('should work correctly with new option names', async () => {
139
+ const response = await supertest(server).get('/test-page.html');
140
+
141
+ expect(response.status).toBe(200);
142
+ expect(response.headers['etag']).toBeDefined();
143
+ expect(response.headers['cache-control']).toContain('max-age=3600');
144
+ });
145
+ });
146
+
147
+ describe('Using both old and new names (new takes precedence)', () => {
148
+ let app;
149
+ let server;
150
+ let consoleWarnSpy;
151
+
152
+ beforeAll(() => {
153
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
154
+
155
+ app = new Koa();
156
+
157
+ // Use BOTH old and new names - new should take precedence
158
+ app.use(koaClassicServer(rootDir, {
159
+ enableCaching: true, // OLD (deprecated)
160
+ browserCacheEnabled: false, // NEW (should take precedence)
161
+ cacheMaxAge: 7200, // OLD (deprecated)
162
+ browserCacheMaxAge: 9999 // NEW (should take precedence)
163
+ }));
164
+
165
+ server = app.listen();
166
+ });
167
+
168
+ afterAll(() => {
169
+ server.close();
170
+ consoleWarnSpy.mockRestore();
171
+ });
172
+
173
+ test('should NOT display warnings when new names are also provided', () => {
174
+ // When both old and new names are provided, no warning should be shown
175
+ const cachingWarnings = consoleWarnSpy.mock.calls.filter(call =>
176
+ call[1] && (call[1].includes('enableCaching') || call[1].includes('cacheMaxAge'))
177
+ );
178
+
179
+ expect(cachingWarnings.length).toBe(0);
180
+ });
181
+
182
+ test('new option values should take precedence over old ones', async () => {
183
+ const response = await supertest(server).get('/test-page.html');
184
+
185
+ expect(response.status).toBe(200);
186
+
187
+ // browserCacheEnabled: false should take precedence over enableCaching: true
188
+ // So there should be NO caching headers
189
+ expect(response.headers['etag']).toBeUndefined();
190
+ expect(response.headers['cache-control']).toContain('no-cache');
191
+ });
192
+ });
193
+
194
+ describe('Default behavior (no caching options specified)', () => {
195
+ let app;
196
+ let server;
197
+
198
+ beforeAll(() => {
199
+ app = new Koa();
200
+ app.use(koaClassicServer(rootDir));
201
+ server = app.listen();
202
+ });
203
+
204
+ afterAll(() => {
205
+ server.close();
206
+ });
207
+
208
+ test('should default to browserCacheEnabled: false', async () => {
209
+ const response = await supertest(server).get('/test-page.html');
210
+
211
+ expect(response.status).toBe(200);
212
+
213
+ // Default is caching disabled
214
+ expect(response.headers['cache-control']).toContain('no-cache');
215
+ });
216
+ });
217
+ });
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Page</h1></body></html>