koa-classic-server 2.1.4 → 2.3.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,30 @@
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-197%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
+ ✅ **Comprehensive Testing** - 197 tests passing with extensive coverage
30
+ ✅ **Complete Documentation** - Detailed guides and examples
38
31
 
39
32
  [See full changelog →](./docs/CHANGELOG.md)
40
33
 
@@ -101,8 +94,8 @@ app.use(koaClassicServer(__dirname + '/public', {
101
94
  showDirContents: true,
102
95
  index: ['index.html', 'index.htm'],
103
96
  urlPrefix: '/static',
104
- cacheMaxAge: 3600,
105
- enableCaching: true
97
+ browserCacheMaxAge: 3600,
98
+ browserCacheEnabled: true
106
99
  }));
107
100
 
108
101
  app.listen(3000);
@@ -229,14 +222,14 @@ Enable aggressive caching for static files:
229
222
 
230
223
  ```javascript
231
224
  app.use(koaClassicServer(__dirname + '/public', {
232
- enableCaching: true, // Enable ETag and Last-Modified
233
- cacheMaxAge: 86400, // Cache for 24 hours (in seconds)
225
+ browserCacheEnabled: true, // Enable ETag and Last-Modified
226
+ browserCacheMaxAge: 86400, // Cache for 24 hours (in seconds)
234
227
  }));
235
228
  ```
236
229
 
237
230
  **⚠️ Important: Production Recommendation**
238
231
 
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:
232
+ 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
233
 
241
234
  - 80-95% bandwidth reduction
242
235
  - 304 Not Modified responses for unchanged files
@@ -281,8 +274,8 @@ app.use(koaClassicServer(path.join(__dirname, 'public'), {
281
274
  index: ['index.html', 'index.htm'],
282
275
  urlPrefix: '/assets',
283
276
  urlsReserved: ['/admin', '/api', '/.git'],
284
- enableCaching: true,
285
- cacheMaxAge: 86400, // 24 hours
277
+ browserCacheEnabled: true,
278
+ browserCacheMaxAge: 86400, // 24 hours
286
279
  }));
287
280
 
288
281
  // Serve dynamic templates
@@ -366,10 +359,18 @@ Creates a Koa middleware for serving static files.
366
359
  ext: ['ejs', 'pug', 'hbs']
367
360
  },
368
361
 
369
- // HTTP caching configuration
362
+ // Browser HTTP caching configuration
370
363
  // 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)
364
+ browserCacheEnabled: false, // Enable ETag & Last-Modified (default: false)
365
+ browserCacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
366
+
367
+ // URL resolution
368
+ useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
369
+ // Set false for URL rewriting middleware (i18n, routing)
370
+
371
+ // DEPRECATED (use new names above):
372
+ // enableCaching: use browserCacheEnabled instead
373
+ // cacheMaxAge: use browserCacheMaxAge instead
373
374
  }
374
375
  ```
375
376
 
@@ -384,8 +385,52 @@ Creates a Koa middleware for serving static files.
384
385
  | `urlsReserved` | Array | `[]` | Reserved directory paths (first-level only) |
385
386
  | `template.render` | Function | `undefined` | Template rendering function |
386
387
  | `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 |
388
+ | `browserCacheEnabled` | Boolean | `false` | Enable browser HTTP caching headers (recommended: `true` in production) |
389
+ | `browserCacheMaxAge` | Number | `3600` | Browser cache duration in seconds |
390
+ | `useOriginalUrl` | Boolean | `true` | Use `ctx.originalUrl` (default) or `ctx.url` for URL resolution |
391
+ | ~~`enableCaching`~~ | Boolean | `false` | **DEPRECATED**: Use `browserCacheEnabled` instead |
392
+ | ~~`cacheMaxAge`~~ | Number | `3600` | **DEPRECATED**: Use `browserCacheMaxAge` instead |
393
+
394
+ #### useOriginalUrl (Boolean, default: true)
395
+
396
+ Controls which URL property is used for file resolution:
397
+ - **`true` (default)**: Uses `ctx.originalUrl` (immutable, reflects the original request)
398
+ - **`false`**: Uses `ctx.url` (mutable, can be modified by middleware)
399
+
400
+ **When to use `false`:**
401
+
402
+ 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.
403
+
404
+ **Example with i18n middleware:**
405
+
406
+ ```javascript
407
+ const Koa = require('koa');
408
+ const koaClassicServer = require('koa-classic-server');
409
+
410
+ const app = new Koa();
411
+
412
+ // i18n middleware that rewrites URLs
413
+ app.use(async (ctx, next) => {
414
+ if (ctx.path.match(/^\/it\//)) {
415
+ ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html → /page.html
416
+ }
417
+ await next();
418
+ });
419
+
420
+ // Serve files using rewritten URL
421
+ app.use(koaClassicServer(__dirname + '/www', {
422
+ useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
423
+ }));
424
+
425
+ app.listen(3000);
426
+ ```
427
+
428
+ **How it works:**
429
+ - Request: `GET /it/page.html`
430
+ - `ctx.originalUrl`: `/it/page.html` (unchanged)
431
+ - `ctx.url`: `/page.html` (rewritten by middleware)
432
+ - With `useOriginalUrl: false`: Server looks for `/www/page.html` ✅
433
+ - With `useOriginalUrl: true` (default): Server looks for `/www/it/page.html` ❌ 404
389
434
 
390
435
  ---
391
436
 
@@ -511,7 +556,7 @@ npm run test:performance
511
556
  ```
512
557
 
513
558
  **Test Coverage:**
514
- - ✅ 153 tests passing
559
+ - ✅ 197 tests passing
515
560
  - ✅ Security tests (path traversal, XSS, race conditions)
516
561
  - ✅ EJS template integration tests
517
562
  - ✅ Index option tests (strings, arrays, RegExp)
@@ -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>
@@ -0,0 +1,213 @@
1
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // TEST FOR useOriginalUrl OPTION
4
+ // This test verifies that the useOriginalUrl option works correctly with URL rewriting middleware
5
+ //
6
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7
+
8
+ const supertest = require('supertest');
9
+ const koaClassicServer = require('../index.cjs');
10
+ const Koa = require('koa');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const rootDir = path.join(__dirname, 'publicWwwTest');
15
+
16
+ // Create a simple test file if it doesn't exist
17
+ const testFilePath = path.join(rootDir, 'test-page.html');
18
+ const testFileContent = '<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Page</h1></body></html>';
19
+
20
+ beforeAll(() => {
21
+ // Ensure test directory exists
22
+ if (!fs.existsSync(rootDir)) {
23
+ fs.mkdirSync(rootDir, { recursive: true });
24
+ }
25
+
26
+ // Create test file
27
+ if (!fs.existsSync(testFilePath)) {
28
+ fs.writeFileSync(testFilePath, testFileContent, 'utf-8');
29
+ }
30
+ });
31
+
32
+ describe('useOriginalUrl option tests', () => {
33
+
34
+ describe('Default behavior (useOriginalUrl: true)', () => {
35
+ let app;
36
+ let server;
37
+
38
+ beforeAll(() => {
39
+ app = new Koa();
40
+
41
+ // i18n middleware that rewrites URLs
42
+ app.use(async (ctx, next) => {
43
+ if (ctx.path.match(/^\/it\//)) {
44
+ // Rewrite /it/page.html to /page.html
45
+ ctx.url = ctx.path.replace(/^\/it/, '');
46
+ }
47
+ await next();
48
+ });
49
+
50
+ // Serve files with default useOriginalUrl: true
51
+ app.use(koaClassicServer(rootDir, {
52
+ useOriginalUrl: true // Default behavior
53
+ }));
54
+
55
+ server = app.listen();
56
+ });
57
+
58
+ afterAll(() => {
59
+ server.close();
60
+ });
61
+
62
+ test('should use ctx.originalUrl (original request path)', async () => {
63
+ // Request /it/test-page.html
64
+ // ctx.originalUrl = /it/test-page.html (unchanged)
65
+ // ctx.url = /test-page.html (rewritten by middleware)
66
+ // With useOriginalUrl: true, server looks for /it/test-page.html (which doesn't exist)
67
+ const response = await supertest(server).get('/it/test-page.html');
68
+
69
+ // Should return 404 because /it/test-page.html doesn't exist
70
+ expect(response.status).toBe(404);
71
+ });
72
+
73
+ test('should serve file without rewriting', async () => {
74
+ // Request /test-page.html directly (no rewriting)
75
+ const response = await supertest(server).get('/test-page.html');
76
+
77
+ // Should return 200 and the file content
78
+ expect(response.status).toBe(200);
79
+ expect(response.text).toContain('Test Page');
80
+ });
81
+ });
82
+
83
+ describe('URL rewriting support (useOriginalUrl: false)', () => {
84
+ let app;
85
+ let server;
86
+
87
+ beforeAll(() => {
88
+ app = new Koa();
89
+
90
+ // i18n middleware that rewrites URLs
91
+ app.use(async (ctx, next) => {
92
+ if (ctx.path.match(/^\/it\//)) {
93
+ // Rewrite /it/page.html to /page.html
94
+ ctx.url = ctx.path.replace(/^\/it/, '');
95
+ }
96
+ await next();
97
+ });
98
+
99
+ // Serve files with useOriginalUrl: false to use rewritten URL
100
+ app.use(koaClassicServer(rootDir, {
101
+ useOriginalUrl: false // Use ctx.url (rewritten)
102
+ }));
103
+
104
+ server = app.listen();
105
+ });
106
+
107
+ afterAll(() => {
108
+ server.close();
109
+ });
110
+
111
+ test('should use ctx.url (rewritten path)', async () => {
112
+ // Request /it/test-page.html
113
+ // ctx.originalUrl = /it/test-page.html (unchanged)
114
+ // ctx.url = /test-page.html (rewritten by middleware)
115
+ // With useOriginalUrl: false, server looks for /test-page.html (which exists)
116
+ const response = await supertest(server).get('/it/test-page.html');
117
+
118
+ // Should return 200 and the file content
119
+ expect(response.status).toBe(200);
120
+ expect(response.text).toContain('Test Page');
121
+ });
122
+
123
+ test('should still serve file without rewriting', async () => {
124
+ // Request /test-page.html directly (no rewriting)
125
+ const response = await supertest(server).get('/test-page.html');
126
+
127
+ // Should return 200 and the file content
128
+ expect(response.status).toBe(200);
129
+ expect(response.text).toContain('Test Page');
130
+ });
131
+ });
132
+
133
+ describe('Complex i18n routing scenario', () => {
134
+ let app;
135
+ let server;
136
+
137
+ beforeAll(() => {
138
+ app = new Koa();
139
+
140
+ // More complex i18n middleware
141
+ app.use(async (ctx, next) => {
142
+ const langPattern = /^\/(it|fr|de|es)\//;
143
+ const match = ctx.path.match(langPattern);
144
+ if (match) {
145
+ // Store language in state
146
+ ctx.state.lang = match[1];
147
+ // Strip language prefix
148
+ ctx.url = ctx.path.replace(langPattern, '/');
149
+ }
150
+ await next();
151
+ });
152
+
153
+ app.use(koaClassicServer(rootDir, {
154
+ useOriginalUrl: false
155
+ }));
156
+
157
+ server = app.listen();
158
+ });
159
+
160
+ afterAll(() => {
161
+ server.close();
162
+ });
163
+
164
+ test('should work with Italian locale (/it/)', async () => {
165
+ const response = await supertest(server).get('/it/test-page.html');
166
+ expect(response.status).toBe(200);
167
+ expect(response.text).toContain('Test Page');
168
+ });
169
+
170
+ test('should work with French locale (/fr/)', async () => {
171
+ const response = await supertest(server).get('/fr/test-page.html');
172
+ expect(response.status).toBe(200);
173
+ expect(response.text).toContain('Test Page');
174
+ });
175
+
176
+ test('should work with German locale (/de/)', async () => {
177
+ const response = await supertest(server).get('/de/test-page.html');
178
+ expect(response.status).toBe(200);
179
+ expect(response.text).toContain('Test Page');
180
+ });
181
+
182
+ test('should work with Spanish locale (/es/)', async () => {
183
+ const response = await supertest(server).get('/es/test-page.html');
184
+ expect(response.status).toBe(200);
185
+ expect(response.text).toContain('Test Page');
186
+ });
187
+ });
188
+
189
+ describe('Backward compatibility', () => {
190
+ let app;
191
+ let server;
192
+
193
+ beforeAll(() => {
194
+ app = new Koa();
195
+
196
+ // No URL rewriting middleware
197
+ // Default useOriginalUrl (should be true)
198
+ app.use(koaClassicServer(rootDir));
199
+
200
+ server = app.listen();
201
+ });
202
+
203
+ afterAll(() => {
204
+ server.close();
205
+ });
206
+
207
+ test('should work with default options (backward compatible)', async () => {
208
+ const response = await supertest(server).get('/test-page.html');
209
+ expect(response.status).toBe(200);
210
+ expect(response.text).toContain('Test Page');
211
+ });
212
+ });
213
+ });
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ All notable changes to koa-classic-server will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.3.0] - 2026-01-03
9
+
10
+ ### 🔄 Renamed Options (with Backward Compatibility)
11
+
12
+ #### Renamed Caching Options for Clarity
13
+ - **Old Names** (DEPRECATED): `enableCaching`, `cacheMaxAge`
14
+ - **New Names**: `browserCacheEnabled`, `browserCacheMaxAge`
15
+ - **Reason**: Improved clarity - these options specifically control browser-side HTTP caching
16
+ - **Backward Compatible**: Old names still work but display deprecation warnings
17
+
18
+ #### Deprecation Warnings
19
+ When using deprecated option names, a warning is displayed on the terminal:
20
+ ```
21
+ [koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.
22
+ Current usage: enableCaching: true
23
+ Recommended: browserCacheEnabled: true
24
+ Please update your configuration to use the new option name.
25
+ ```
26
+
27
+ ### 📝 Documentation Updates
28
+
29
+ - Updated README.md with new option names
30
+ - Updated JSDoc comments in index.cjs
31
+ - Added deprecation notes in Options table
32
+ - All examples updated to use new names
33
+
34
+ ### 🔧 Changes
35
+
36
+ - **index.cjs**: Lines 109-135 - Added backward compatibility logic with deprecation warnings
37
+ - **index.cjs**: Lines 47-58 - Updated JSDoc comments
38
+ - **index.cjs**: Lines 350, 361 - Updated code to use new option names
39
+ - **README.md**: Updated all references to use new names, added deprecation notes
40
+ - **package.json**: Version bumped from `2.2.0` to `2.3.0`
41
+
42
+ ### ⚠️ Migration Guide
43
+
44
+ **No immediate changes required** - old option names still work.
45
+
46
+ **Recommended migration:**
47
+
48
+ ```javascript
49
+ // Old (still works, but deprecated)
50
+ app.use(koaClassicServer('/public', {
51
+ enableCaching: true,
52
+ cacheMaxAge: 3600
53
+ }));
54
+
55
+ // New (recommended)
56
+ app.use(koaClassicServer('/public', {
57
+ browserCacheEnabled: true,
58
+ browserCacheMaxAge: 3600
59
+ }));
60
+ ```
61
+
62
+ **Timeline:**
63
+ - **v2.3.0**: Old names work with deprecation warnings
64
+ - **Future versions**: Old names may be removed (will be announced in advance)
65
+
66
+ ### 📦 Package Changes
67
+
68
+ - **Version**: `2.2.0` → `2.3.0`
69
+ - **Semver**: Minor version bump (new feature names, backward compatible)
70
+
71
+ ---
72
+
73
+ ## [2.2.0] - 2026-01-03
74
+
75
+ ### ✨ Features
76
+
77
+ #### Added useOriginalUrl Option
78
+ - **New Option**: `useOriginalUrl` (Boolean, default: `true`)
79
+ - **Purpose**: Controls URL resolution for file serving - use `ctx.originalUrl` (immutable) or `ctx.url` (mutable)
80
+ - **Use Case**: Compatibility with URL rewriting middleware (i18n, routing)
81
+ - **Backward Compatible**: Default value `true` maintains existing behavior
82
+
83
+ #### URL Rewriting Middleware Support
84
+ - **Problem Solved**: koa-classic-server previously used `ctx.href` (based on `ctx.originalUrl`), which caused 404 errors when middleware rewrites URLs by modifying `ctx.url`
85
+ - **Solution**: Set `useOriginalUrl: false` to use the rewritten URL from `ctx.url` instead
86
+ - **Example**: i18n middleware that strips language prefixes (`/it/page.html` → `/page.html`)
87
+
88
+ ### 📝 Documentation
89
+
90
+ - Added comprehensive `useOriginalUrl` documentation in README.md
91
+ - Added JSDoc comments in index.cjs
92
+ - Included practical i18n middleware example
93
+ - Added option to API reference table
94
+
95
+ ### 🔧 Changes
96
+
97
+ - **index.cjs**: Line 108 - Added `useOriginalUrl` option initialization
98
+ - **index.cjs**: Lines 117-125 - Modified URL construction logic to support both `ctx.originalUrl` and `ctx.url`
99
+ - **README.md**: Added detailed section explaining `useOriginalUrl` with examples
100
+ - **package.json**: Version bumped from `2.1.4` to `2.2.0`
101
+
102
+ ### 💡 Usage Example
103
+
104
+ ```javascript
105
+ // i18n middleware example
106
+ app.use(async (ctx, next) => {
107
+ if (ctx.path.match(/^\/it\//)) {
108
+ ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html → /page.html
109
+ }
110
+ await next();
111
+ });
112
+
113
+ app.use(koaClassicServer('/www', {
114
+ useOriginalUrl: false // Use rewritten URL
115
+ }));
116
+ ```
117
+
118
+ ### ⚠️ Migration Notes
119
+
120
+ **No breaking changes** - this is a backward-compatible release.
121
+
122
+ - **Default behavior unchanged**: `useOriginalUrl` defaults to `true`
123
+ - **No code changes required** for existing implementations
124
+ - **New feature**: Set `useOriginalUrl: false` if you use URL rewriting middleware
125
+
126
+ ### 📦 Package Changes
127
+
128
+ - **Version**: `2.1.4` → `2.2.0`
129
+ - **Semver**: Minor version bump (new feature, backward compatible)
130
+
131
+ ---
132
+
8
133
  ## [2.1.3] - 2025-11-25
9
134
 
10
135
  ### 🔧 Configuration Changes
package/index.cjs CHANGED
@@ -44,11 +44,17 @@ module.exports = function koaClassicServer(
44
44
  render: undefined, // Template rendering function: async (ctx, next, filePath) => {}
45
45
  ext: [], // File extensions to process with template.render
46
46
  },
47
- cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 1 hour)
48
- enableCaching: false, // Enable HTTP caching headers (ETag, Last-Modified)
49
- // NOTE: Default is false for development.
50
- // In production, it's recommended to set enableCaching: true
51
- // to reduce bandwidth usage and improve performance.
47
+ browserCacheMaxAge: 3600, // Browser Cache-Control max-age in seconds (default: 1 hour)
48
+ browserCacheEnabled: false, // Enable browser HTTP caching headers (ETag, Last-Modified)
49
+ // NOTE: Default is false for development.
50
+ // In production, it's recommended to set browserCacheEnabled: true
51
+ // to reduce bandwidth usage and improve performance.
52
+ useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
53
+ // Set false for URL rewriting middleware (i18n, routing)
54
+
55
+ // DEPRECATED OPTIONS (maintained for backward compatibility):
56
+ // cacheMaxAge: use browserCacheMaxAge instead
57
+ // enableCaching: use browserCacheEnabled instead
52
58
  }
53
59
  */
54
60
  ) {
@@ -100,11 +106,37 @@ module.exports = function koaClassicServer(
100
106
  options.template.ext = Array.isArray(options.template.ext) ? options.template.ext : [];
101
107
 
102
108
  // OPTIMIZATION: HTTP Caching options
103
- // NOTE: Default enableCaching is false for development environments.
109
+ // NOTE: Default browserCacheEnabled is false for development environments.
104
110
  // For production deployments, it's strongly recommended to enable caching
105
- // by setting enableCaching: true to benefit from reduced bandwidth and improved performance.
106
- options.cacheMaxAge = typeof options.cacheMaxAge === 'number' && options.cacheMaxAge >= 0 ? options.cacheMaxAge : 3600;
107
- options.enableCaching = typeof options.enableCaching === 'boolean' ? options.enableCaching : false;
111
+ // by setting browserCacheEnabled: true to benefit from reduced bandwidth and improved performance.
112
+
113
+ // DEPRECATION: Handle legacy option names for backward compatibility
114
+ if ('cacheMaxAge' in opts && !('browserCacheMaxAge' in opts)) {
115
+ console.warn(
116
+ '\x1b[33m%s\x1b[0m',
117
+ '[koa-classic-server] DEPRECATION WARNING: The "cacheMaxAge" option is deprecated and will be removed in future versions.\n' +
118
+ ' Current usage: cacheMaxAge: ' + opts.cacheMaxAge + '\n' +
119
+ ' Recommended: browserCacheMaxAge: ' + opts.cacheMaxAge + '\n' +
120
+ ' Please update your configuration to use the new option name.'
121
+ );
122
+ options.browserCacheMaxAge = opts.cacheMaxAge;
123
+ }
124
+
125
+ if ('enableCaching' in opts && !('browserCacheEnabled' in opts)) {
126
+ console.warn(
127
+ '\x1b[33m%s\x1b[0m',
128
+ '[koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.\n' +
129
+ ' Current usage: enableCaching: ' + opts.enableCaching + '\n' +
130
+ ' Recommended: browserCacheEnabled: ' + opts.enableCaching + '\n' +
131
+ ' Please update your configuration to use the new option name.'
132
+ );
133
+ options.browserCacheEnabled = opts.enableCaching;
134
+ }
135
+
136
+ // Set new option names (with defaults)
137
+ options.browserCacheMaxAge = typeof options.browserCacheMaxAge === 'number' && options.browserCacheMaxAge >= 0 ? options.browserCacheMaxAge : 3600;
138
+ options.browserCacheEnabled = typeof options.browserCacheEnabled === 'boolean' ? options.browserCacheEnabled : false;
139
+ options.useOriginalUrl = typeof options.useOriginalUrl === 'boolean' ? options.useOriginalUrl : true;
108
140
 
109
141
  return async (ctx, next) => {
110
142
  // Check if method is allowed
@@ -113,12 +145,14 @@ module.exports = function koaClassicServer(
113
145
  return;
114
146
  }
115
147
 
116
- // Normalize URL (remove trailing slash)
148
+ // Construct full URL based on useOriginalUrl option
149
+ const urlToUse = options.useOriginalUrl ? ctx.originalUrl : ctx.url;
150
+ const fullUrl = ctx.protocol + '://' + ctx.host + urlToUse;
117
151
  let pageHref = '';
118
- if (ctx.href.charAt(ctx.href.length - 1) === '/') {
119
- pageHref = new URL(ctx.href.slice(0, -1));
152
+ if (fullUrl.charAt(fullUrl.length - 1) === '/') {
153
+ pageHref = new URL(fullUrl.slice(0, -1));
120
154
  } else {
121
- pageHref = new URL(ctx.href);
155
+ pageHref = new URL(fullUrl);
122
156
  }
123
157
 
124
158
  // Check URL prefix
@@ -317,7 +351,7 @@ module.exports = function koaClassicServer(
317
351
  }
318
352
 
319
353
  // OPTIMIZATION: HTTP Caching Headers
320
- if (options.enableCaching) {
354
+ if (options.browserCacheEnabled) {
321
355
  // Generate ETag from mtime timestamp + file size
322
356
  // This ensures ETag changes when file is modified or resized
323
357
  const etag = `"${fileStat.mtime.getTime()}-${fileStat.size}"`;
@@ -328,7 +362,7 @@ module.exports = function koaClassicServer(
328
362
  // Set caching headers
329
363
  ctx.set('ETag', etag);
330
364
  ctx.set('Last-Modified', lastModified);
331
- ctx.set('Cache-Control', `public, max-age=${options.cacheMaxAge}, must-revalidate`);
365
+ ctx.set('Cache-Control', `public, max-age=${options.browserCacheMaxAge}, must-revalidate`);
332
366
 
333
367
  // OPTIMIZATION: Handle conditional requests (304 Not Modified)
334
368
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa-classic-server",
3
- "version": "2.1.4",
3
+ "version": "2.3.0",
4
4
  "description": "High-performance Koa middleware for serving static files with Apache-like directory listing, HTTP caching, template engine support, and comprehensive security fixes",
5
5
  "main": "index.cjs",
6
6
  "exports": {